From 37d5ef75306c45ca95550f4af40d9750c5b9cb88 Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Mon, 6 May 2024 13:42:55 +0200 Subject: initial commit yup, i definitely wrote all of this in a single commit --- src/widgets/notification-widget.c | 821 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 821 insertions(+) create mode 100644 src/widgets/notification-widget.c (limited to 'src/widgets/notification-widget.c') 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 -- cgit v1.3.1