summaryrefslogtreecommitdiff
path: root/src/widgets/notification-widget.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/widgets/notification-widget.c')
-rw-r--r--src/widgets/notification-widget.c821
1 files changed, 821 insertions, 0 deletions
diff --git a/src/widgets/notification-widget.c b/src/widgets/notification-widget.c
new file mode 100644
index 0000000..477a57c
--- /dev/null
+++ b/src/widgets/notification-widget.c
@@ -0,0 +1,821 @@
+#include "widgets/notification-widget.h"
+
+//
+// FoobarNotificationCloseAction:
+//
+// The action invoked when the user requests to "close" the notification. The notification is either marked as
+// "dismissed" and kept around or removed from the list.
+//
+
+G_DEFINE_ENUM_TYPE(
+ FoobarNotificationCloseAction,
+ foobar_notification_close_action,
+ G_DEFINE_ENUM_VALUE( FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE, "remove" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS, "dismiss" ) )
+
+//
+// FoobarNotificationWidget:
+//
+// Shared widget for displaying a notification. The widget also manages an inset/margin -- this is done to allow the
+// "close" button to extend into the margin area.
+//
+
+struct _FoobarNotificationWidget
+{
+ GtkWidget parent_instance;
+ GtkWidget* container;
+ GtkWidget* content;
+ GtkWidget* close_button;
+ FoobarNotification* notification;
+ FoobarNotificationCloseAction close_action;
+ gchar* time_format;
+ gint min_height;
+ gint close_button_inset;
+ gint inset_start;
+ gint inset_end;
+ gint inset_top;
+ gint inset_bottom;
+};
+
+enum
+{
+ PROP_NOTIFICATION = 1,
+ PROP_CLOSE_ACTION,
+ PROP_TIME_FORMAT,
+ PROP_MIN_HEIGHT,
+ PROP_CLOSE_BUTTON_INSET,
+ PROP_INSET_START,
+ PROP_INSET_END,
+ PROP_INSET_TOP,
+ PROP_INSET_BOTTOM,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_notification_widget_class_init ( FoobarNotificationWidgetClass* klass );
+static void foobar_notification_widget_init ( FoobarNotificationWidget* self );
+static void foobar_notification_widget_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_notification_widget_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_notification_widget_dispose ( GObject* object );
+static void foobar_notification_widget_finalize ( GObject* object );
+static void foobar_notification_widget_handle_enter ( GtkEventControllerMotion* controller,
+ gdouble x,
+ gdouble y,
+ gpointer userdata );
+static void foobar_notification_widget_handle_leave ( GtkEventControllerMotion* controller,
+ gpointer userdata );
+static void foobar_notification_widget_handle_close_clicked( GtkButton* button,
+ gpointer userdata );
+static gchar* foobar_notification_widget_compute_time_label ( GtkExpression* expression,
+ GDateTime* time,
+ gchar const* format,
+ gpointer userdata );
+static gboolean foobar_notification_widget_compute_icon_visible( GtkExpression* expression,
+ GIcon* icon,
+ gpointer userdata );
+static gboolean foobar_notification_widget_compute_body_visible( GtkExpression* expression,
+ gchar const* body,
+ gpointer userdata );
+static void foobar_notification_widget_update_margins ( FoobarNotificationWidget* self );
+
+G_DEFINE_FINAL_TYPE( FoobarNotificationWidget, foobar_notification_widget, GTK_TYPE_WIDGET )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Widget Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for notifications.
+//
+void foobar_notification_widget_class_init( FoobarNotificationWidgetClass* klass )
+{
+ GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass );
+ gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BIN_LAYOUT );
+ gtk_widget_class_set_css_name( widget_klass, "notification" );
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_notification_widget_get_property;
+ object_klass->set_property = foobar_notification_widget_set_property;
+ object_klass->dispose = foobar_notification_widget_dispose;
+ object_klass->finalize = foobar_notification_widget_finalize;
+
+ props[PROP_NOTIFICATION] = g_param_spec_object(
+ "notification",
+ "Notification",
+ "Content to display.",
+ FOOBAR_TYPE_NOTIFICATION,
+ G_PARAM_READWRITE );
+ props[PROP_CLOSE_ACTION] = g_param_spec_enum(
+ "close-action",
+ "Close Action",
+ "Action to execute when the notification is closed.",
+ FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_TIME_FORMAT] = g_param_spec_string(
+ "time-format",
+ "Time Format",
+ "Time format used to display the notification's timestamp.",
+ NULL,
+ G_PARAM_READWRITE );
+ props[PROP_MIN_HEIGHT] = g_param_spec_int(
+ "min-height",
+ "Minimum Height",
+ "Minimum requested height for the notification itself.",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_CLOSE_BUTTON_INSET] = g_param_spec_int(
+ "close-button-inset",
+ "Close Button Inset",
+ "Offset of the close button within the notification (may be negative, possibly increasing the actual inset).",
+ INT_MIN,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_INSET_START] = g_param_spec_int(
+ "inset-start",
+ "Inset Start",
+ "Inset for the start edge of the widget.",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_INSET_END] = g_param_spec_int(
+ "inset-end",
+ "Inset End",
+ "Inset for the end edge of the widget.",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_INSET_TOP] = g_param_spec_int(
+ "inset-top",
+ "Inset Top",
+ "Inset for the top edge of the widget.",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_INSET_BOTTOM] = g_param_spec_int(
+ "inset-bottom",
+ "Inset Bottom",
+ "Inset for the bottom edge of the widget.",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for notifications.
+//
+void foobar_notification_widget_init( FoobarNotificationWidget* self )
+{
+ // Set up the main notification content.
+
+ GtkWidget* icon = gtk_image_new( );
+ gtk_widget_add_css_class( icon, "icon" );
+ gtk_widget_set_valign( icon, GTK_ALIGN_CENTER );
+
+ GtkWidget* title = gtk_label_new( NULL );
+ gtk_label_set_justify( GTK_LABEL( title ), GTK_JUSTIFY_LEFT );
+ gtk_label_set_ellipsize( GTK_LABEL( title ), PANGO_ELLIPSIZE_END );
+ gtk_label_set_wrap( GTK_LABEL( title ), FALSE );
+ gtk_label_set_xalign( GTK_LABEL( title ), 0 );
+ gtk_widget_add_css_class( title, "title" );
+ gtk_widget_set_valign( title, GTK_ALIGN_BASELINE_CENTER );
+ gtk_widget_set_hexpand( title, TRUE );
+
+ GtkWidget* time = gtk_label_new( NULL );
+ gtk_label_set_wrap( GTK_LABEL( time ), FALSE );
+ gtk_widget_add_css_class( time, "time" );
+ gtk_widget_set_valign( time, GTK_ALIGN_BASELINE_CENTER );
+
+ GtkWidget* body = gtk_label_new( NULL );
+ gtk_label_set_use_markup( GTK_LABEL( body ), TRUE );
+ gtk_label_set_justify( GTK_LABEL( body ), GTK_JUSTIFY_LEFT );
+ gtk_label_set_ellipsize( GTK_LABEL( body ), PANGO_ELLIPSIZE_END );
+ gtk_label_set_wrap( GTK_LABEL( body ), TRUE );
+ gtk_label_set_wrap_mode( GTK_LABEL( body ), PANGO_WRAP_WORD_CHAR );
+ // XXX: Increase once layout is fixed in GTK (https://gitlab.gnome.org/GNOME/gtk/-/issues/5885)
+ gtk_label_set_lines( GTK_LABEL( body ), 1 );
+ gtk_label_set_xalign( GTK_LABEL( body ), 0 );
+ gtk_widget_add_css_class( body, "body" );
+
+ GtkWidget* header = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_box_append( GTK_BOX( header ), title );
+ gtk_box_append( GTK_BOX( header ), time );
+
+ GtkWidget* row = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_box_append( GTK_BOX( row ), header );
+ gtk_box_append( GTK_BOX( row ), body );
+ gtk_widget_set_hexpand( row, TRUE );
+ gtk_widget_set_valign( row, GTK_ALIGN_CENTER );
+
+ self->content = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_box_append( GTK_BOX( self->content ), icon );
+ gtk_box_append( GTK_BOX( self->content ), row );
+ gtk_widget_add_css_class( self->content, "content" );
+
+ // Set up the overlay for the close button.
+
+ GtkWidget* close_icon = gtk_image_new_from_icon_name( "fluent-dismiss-symbolic" );
+ gtk_image_set_pixel_size( GTK_IMAGE( close_icon ), 12 );
+
+ self->close_button = gtk_button_new( );
+ gtk_button_set_child( GTK_BUTTON( self->close_button ), close_icon );
+ gtk_widget_add_css_class( self->close_button, "close-button" );
+ gtk_widget_set_visible( self->close_button, FALSE );
+ gtk_widget_set_halign( self->close_button, GTK_ALIGN_END );
+ gtk_widget_set_valign( self->close_button, GTK_ALIGN_START );
+ g_signal_connect( self->close_button, "clicked", G_CALLBACK( foobar_notification_widget_handle_close_clicked ), self );
+
+ GtkEventController* hover_controller = gtk_event_controller_motion_new( );
+ g_signal_connect( hover_controller, "enter", G_CALLBACK( foobar_notification_widget_handle_enter ), self );
+ g_signal_connect( hover_controller, "leave", G_CALLBACK( foobar_notification_widget_handle_leave ), self );
+
+ self->container = gtk_overlay_new( );
+ gtk_overlay_set_child( GTK_OVERLAY( self->container ), self->content );
+ gtk_overlay_add_overlay( GTK_OVERLAY( self->container ), self->close_button );
+ gtk_widget_add_controller( self->container, hover_controller );
+ gtk_widget_set_parent( self->container, GTK_WIDGET( self ) );
+
+ // Set up bindings.
+
+ {
+ GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" );
+ GtkExpression* image_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "image" );
+ gtk_expression_bind( gtk_expression_ref( image_expr ), icon, "gicon", self );
+ GtkExpression* visible_params[] = { image_expr };
+ GtkExpression* visible_expr = gtk_cclosure_expression_new(
+ G_TYPE_BOOLEAN,
+ NULL,
+ G_N_ELEMENTS( visible_params ),
+ visible_params,
+ G_CALLBACK( foobar_notification_widget_compute_icon_visible ),
+ NULL,
+ NULL );
+ gtk_expression_bind( visible_expr, icon, "visible", self );
+ }
+
+ {
+ GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" );
+ GtkExpression* summary_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "summary" );
+ gtk_expression_bind( summary_expr, title, "label", self );
+ }
+
+ {
+ GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" );
+ GtkExpression* body_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "body" );
+ gtk_expression_bind( gtk_expression_ref( body_expr ), body, "label", self );
+ GtkExpression* visible_params[] = { body_expr };
+ GtkExpression* visible_expr = gtk_cclosure_expression_new(
+ G_TYPE_BOOLEAN,
+ NULL,
+ G_N_ELEMENTS( visible_params ),
+ visible_params,
+ G_CALLBACK( foobar_notification_widget_compute_body_visible ),
+ NULL,
+ NULL );
+ gtk_expression_bind( visible_expr, body, "visible", self );
+ }
+
+ {
+ GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" );
+ GtkExpression* time_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "time" );
+ GtkExpression* format_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "time-format" );
+ GtkExpression* str_params[] = { time_expr, format_expr };
+ GtkExpression* str_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( str_params ),
+ str_params,
+ G_CALLBACK( foobar_notification_widget_compute_time_label ),
+ self,
+ NULL );
+ gtk_expression_bind( str_expr, time, "label", self );
+ }
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_notification_widget_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_NOTIFICATION:
+ g_value_set_object( value, foobar_notification_widget_get_notification( self ) );
+ break;
+ case PROP_CLOSE_ACTION:
+ g_value_set_enum( value, foobar_notification_widget_get_close_action( self ) );
+ break;
+ case PROP_TIME_FORMAT:
+ g_value_set_string( value, foobar_notification_widget_get_time_format( self ) );
+ break;
+ case PROP_MIN_HEIGHT:
+ g_value_set_int( value, foobar_notification_widget_get_min_height( self ) );
+ break;
+ case PROP_CLOSE_BUTTON_INSET:
+ g_value_set_int( value, foobar_notification_widget_get_close_button_inset( self ) );
+ break;
+ case PROP_INSET_START:
+ g_value_set_int( value, foobar_notification_widget_get_inset_start( self ) );
+ break;
+ case PROP_INSET_END:
+ g_value_set_int( value, foobar_notification_widget_get_inset_end( self ) );
+ break;
+ case PROP_INSET_TOP:
+ g_value_set_int( value, foobar_notification_widget_get_inset_top( self ) );
+ break;
+ case PROP_INSET_BOTTOM:
+ g_value_set_int( value, foobar_notification_widget_get_inset_bottom( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_notification_widget_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_NOTIFICATION:
+ foobar_notification_widget_set_notification( self, g_value_get_object( value ) );
+ break;
+ case PROP_CLOSE_ACTION:
+ foobar_notification_widget_set_close_action( self, g_value_get_enum( value ) );
+ break;
+ case PROP_TIME_FORMAT:
+ foobar_notification_widget_set_time_format( self, g_value_get_string( value ) );
+ break;
+ case PROP_MIN_HEIGHT:
+ foobar_notification_widget_set_min_height( self, g_value_get_int( value ) );
+ break;
+ case PROP_CLOSE_BUTTON_INSET:
+ foobar_notification_widget_set_close_button_inset( self, g_value_get_int( value ) );
+ break;
+ case PROP_INSET_START:
+ foobar_notification_widget_set_inset_start( self, g_value_get_int( value ) );
+ break;
+ case PROP_INSET_END:
+ foobar_notification_widget_set_inset_end( self, g_value_get_int( value ) );
+ break;
+ case PROP_INSET_TOP:
+ foobar_notification_widget_set_inset_top( self, g_value_get_int( value ) );
+ break;
+ case PROP_INSET_BOTTOM:
+ foobar_notification_widget_set_inset_bottom( self, g_value_get_int( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for notifications.
+//
+void foobar_notification_widget_dispose( GObject* object )
+{
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)object;
+
+ GtkWidget* child;
+ while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) )
+ {
+ gtk_widget_unparent( child );
+ }
+
+ G_OBJECT_CLASS( foobar_notification_widget_parent_class )->dispose( object );
+}
+
+//
+// Instance cleanup for notifications.
+//
+void foobar_notification_widget_finalize( GObject* object )
+{
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)object;
+
+ g_clear_object( &self->notification );
+ g_clear_pointer( &self->time_format, g_free );
+
+ G_OBJECT_CLASS( foobar_notification_widget_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new notification widget instance.
+//
+GtkWidget* foobar_notification_widget_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL );
+}
+
+//
+// Get the notification model currently displayed.
+//
+FoobarNotification* foobar_notification_widget_get_notification( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), NULL );
+ return self->notification;
+}
+
+//
+// Get the action invoked when the user wants to "close" a notification.
+//
+FoobarNotificationCloseAction foobar_notification_widget_get_close_action( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->close_action;
+}
+
+//
+// Get the time format string used to display the notification's timestamp.
+//
+gchar const* foobar_notification_widget_get_time_format( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), NULL );
+ return self->time_format;
+}
+
+//
+// Get the minimum height of the notification (excluding the inset).
+//
+gint foobar_notification_widget_get_min_height( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->min_height;
+}
+
+//
+// Get the offset of the close button within the notification (may be negative, possibly increasing the actual inset).
+//
+gint foobar_notification_widget_get_close_button_inset( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->close_button_inset;
+}
+
+//
+// Get the margin for the start edge of the notification.
+//
+gint foobar_notification_widget_get_inset_start( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->inset_start;
+}
+
+//
+// Get the margin for the end edge of the notification.
+//
+gint foobar_notification_widget_get_inset_end( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->inset_end;
+}
+
+//
+// Get the margin for the top edge of the notification.
+//
+gint foobar_notification_widget_get_inset_top( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->inset_top;
+}
+
+//
+// Get the margin for the bottom edge of the notification.
+//
+gint foobar_notification_widget_get_inset_bottom( FoobarNotificationWidget* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 );
+ return self->inset_bottom;
+}
+
+//
+// Update the notification model currently displayed.
+//
+void foobar_notification_widget_set_notification(
+ FoobarNotificationWidget* self,
+ FoobarNotification* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ if ( self->notification != value )
+ {
+ g_clear_object( &self->notification );
+ if ( value ) { self->notification = g_object_ref( value ); }
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_NOTIFICATION] );
+ }
+}
+
+//
+// Update the action invoked when the user wants to "close" a notification.
+//
+void foobar_notification_widget_set_close_action(
+ FoobarNotificationWidget* self,
+ FoobarNotificationCloseAction value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ if ( self->close_action != value )
+ {
+ self->close_action = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CLOSE_ACTION] );
+ }
+}
+
+//
+// Update the time format string used to display the notification's timestamp.
+//
+void foobar_notification_widget_set_time_format(
+ FoobarNotificationWidget* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ if ( g_strcmp0( self->time_format, value ) )
+ {
+ g_clear_pointer( &self->time_format, g_free );
+ self->time_format = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_TIME_FORMAT] );
+ }
+}
+
+//
+// Update the minimum height of the notification (excluding the inset).
+//
+void foobar_notification_widget_set_min_height(
+ FoobarNotificationWidget* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ value = MAX( value, 0 );
+ if ( self->min_height != value )
+ {
+ self->min_height = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_MIN_HEIGHT] );
+ gtk_widget_set_size_request( self->content, -1, self->min_height );
+ }
+}
+
+//
+// Update the offset of the close button within the notification (may be negative, possibly increasing the actual inset).
+//
+void foobar_notification_widget_set_close_button_inset(
+ FoobarNotificationWidget* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ if ( self->close_button_inset != value )
+ {
+ self->close_button_inset = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CLOSE_BUTTON_INSET] );
+ foobar_notification_widget_update_margins( self );
+ }
+}
+
+//
+// Update the margin for the start edge of the notification.
+//
+void foobar_notification_widget_set_inset_start(
+ FoobarNotificationWidget* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ value = MAX( value, 0 );
+ if ( self->inset_start != value )
+ {
+ self->inset_start = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_START] );
+ foobar_notification_widget_update_margins( self );
+ }
+}
+
+//
+// Update the margin for the end edge of the notification.
+//
+void foobar_notification_widget_set_inset_end(
+ FoobarNotificationWidget* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ value = MAX( value, 0 );
+ if ( self->inset_end != value )
+ {
+ self->inset_end = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_END] );
+ foobar_notification_widget_update_margins( self );
+ }
+}
+
+//
+// Update the margin for the top edge of the notification.
+//
+void foobar_notification_widget_set_inset_top(
+ FoobarNotificationWidget* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ value = MAX( value, 0 );
+ if ( self->inset_top != value )
+ {
+ self->inset_top = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_TOP] );
+ foobar_notification_widget_update_margins( self );
+ }
+}
+
+//
+// Update the margin for the bottom edge of the notification.
+//
+void foobar_notification_widget_set_inset_bottom(
+ FoobarNotificationWidget* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) );
+
+ value = MAX( value, 0 );
+ if ( self->inset_bottom != value )
+ {
+ self->inset_bottom = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_BOTTOM] );
+ foobar_notification_widget_update_margins( self );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called when the mouse cursor enters the notification.
+//
+// We will temporarily block the dismissal timeout for the notification and show the close button.
+//
+void foobar_notification_widget_handle_enter(
+ GtkEventControllerMotion* controller,
+ gdouble x,
+ gdouble y,
+ gpointer userdata )
+{
+ (void)controller;
+ (void)x;
+ (void)y;
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)userdata;
+
+ gtk_widget_set_visible( self->close_button, TRUE );
+ if ( self->notification ) { foobar_notification_block_timeout( self->notification ); }
+}
+
+//
+// Called when the mouse cursor leaves the notification.
+//
+// We will unblock the dismissal timeout for the notification and hide the close button.
+//
+void foobar_notification_widget_handle_leave(
+ GtkEventControllerMotion* controller,
+ gpointer userdata )
+{
+ (void)controller;
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)userdata;
+
+ gtk_widget_set_visible( self->close_button, FALSE );
+ if ( self->notification ) { foobar_notification_resume_timeout( self->notification ); }
+}
+
+//
+// Called when the user has clicked the "close" button.
+//
+// This either marks the notification as dismissed or removes it.
+//
+void foobar_notification_widget_handle_close_clicked(
+ GtkButton* button,
+ gpointer userdata )
+{
+ (void)button;
+ FoobarNotificationWidget* self = (FoobarNotificationWidget*)userdata;
+
+ if ( self->notification )
+ {
+ switch ( self->close_action )
+ {
+ case FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE:
+ foobar_notification_close( self->notification );
+ break;
+ case FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS:
+ foobar_notification_dismiss( self->notification );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Value Converters
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Derive the timestamp label value from the notification's timestamp and the selected time format.
+//
+gchar* foobar_notification_widget_compute_time_label(
+ GtkExpression* expression,
+ GDateTime* time,
+ gchar const* format,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ return format != NULL ? g_date_time_format( time, format ) : NULL;
+}
+
+//
+// Derive the visibility of the notification icon from its value.
+//
+gboolean foobar_notification_widget_compute_icon_visible(
+ GtkExpression* expression,
+ GIcon* icon,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ return icon != NULL;
+}
+
+//
+// Derive the visibility of the notification body label from its value.
+//
+gboolean foobar_notification_widget_compute_body_visible(
+ GtkExpression* expression,
+ gchar const* body,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ return body != NULL && g_strcmp0( body, "" );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Helper Methods
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Update the margins of the content widget and close button to the current inset values.
+//
+void foobar_notification_widget_update_margins( FoobarNotificationWidget* self )
+{
+ gint inset = MAX( self->close_button_inset, 0 );
+ gint overrun = MAX( -self->close_button_inset, 0 );
+ gint actual_inset_end = MAX( self->inset_end, overrun );
+ gint actual_inset_top = MAX( self->inset_top, overrun );
+
+ gtk_widget_set_margin_start( self->container, self->inset_start );
+ gtk_widget_set_margin_end( self->container, actual_inset_end - overrun );
+ gtk_widget_set_margin_top( self->container, actual_inset_top - overrun );
+ gtk_widget_set_margin_bottom( self->container, self->inset_bottom );
+
+ gtk_widget_set_margin_end( self->content, overrun );
+ gtk_widget_set_margin_top( self->content, overrun );
+
+ gtk_widget_set_margin_end( self->close_button, inset );
+ gtk_widget_set_margin_top( self->close_button, inset );
+} \ No newline at end of file