diff options
| author | Hannes Schulze <haschu0103@outlook.de> | 2024-05-06 13:42:55 +0200 |
|---|---|---|
| committer | Hannes Schulze <haschu0103@outlook.de> | 2024-05-06 14:33:54 +0200 |
| commit | 37d5ef75306c45ca95550f4af40d9750c5b9cb88 (patch) | |
| tree | 7125c8e8445eeecb86097bc99bb2d1febfb7e2a5 /src/widgets | |
initial commit
yup, i definitely wrote all of this in a single commit
Diffstat (limited to 'src/widgets')
27 files changed, 5086 insertions, 0 deletions
diff --git a/src/widgets/control-center/control-button.c b/src/widgets/control-center/control-button.c new file mode 100644 index 0000000..f535a61 --- /dev/null +++ b/src/widgets/control-center/control-button.c @@ -0,0 +1,496 @@ +#include "widgets/control-center/control-button.h" + +// +// FoobarControlButton: +// +// A button in the "controls" section of the control center. This can be a regular button or a toggle button with an +// optional secondary button to toggle a details view. +// + +struct _FoobarControlButton +{ + GtkWidget parent_instance; + gchar* icon_name; + gchar* label; + gboolean can_expand; + gboolean can_toggle; + gboolean is_expanded; + gboolean is_toggled; +}; + +enum +{ + PROP_ICON_NAME = 1, + PROP_LABEL, + PROP_CAN_EXPAND, + PROP_CAN_TOGGLE, + PROP_IS_EXPANDED, + PROP_IS_TOGGLED, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_control_button_class_init ( FoobarControlButtonClass* klass ); +static void foobar_control_button_init ( FoobarControlButton* self ); +static void foobar_control_button_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_control_button_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_control_button_dispose ( GObject* object ); +static void foobar_control_button_finalize ( GObject* object ); +static void foobar_control_button_handle_primary_clicked( GtkButton* button, + gpointer userdata ); +static void foobar_control_button_handle_expand_clicked ( GtkButton* button, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarControlButton, foobar_control_button, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for control buttons. +// +void foobar_control_button_class_init( FoobarControlButtonClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + gtk_widget_class_set_css_name( widget_klass, "control-button" ); + gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_control_button_get_property; + object_klass->set_property = foobar_control_button_set_property; + object_klass->dispose = foobar_control_button_dispose; + object_klass->finalize = foobar_control_button_finalize; + + props[PROP_ICON_NAME] = g_param_spec_string( + "icon-name", + "Icon Name", + "Name of the icon to be displayed next to the label.", + NULL, + G_PARAM_READWRITE ); + props[PROP_LABEL] = g_param_spec_string( + "label", + "Label", + "The button's main label.", + NULL, + G_PARAM_READWRITE ); + props[PROP_CAN_EXPAND] = g_param_spec_boolean( + "can-expand", + "Can Expand", + "Indicates whether some details are hidden and can be revealed by a toggle button next to the main action.", + FALSE, + G_PARAM_READWRITE ); + props[PROP_CAN_TOGGLE] = g_param_spec_boolean( + "can-toggle", + "Can Toggle", + "Indicates whether the main button can be toggled.", + FALSE, + G_PARAM_READWRITE ); + props[PROP_IS_EXPANDED] = g_param_spec_boolean( + "is-expanded", + "Is Expanded", + "Indicates whether additional details should be shown.", + FALSE, + G_PARAM_READWRITE ); + props[PROP_IS_TOGGLED] = g_param_spec_boolean( + "is-toggled", + "Is Toggled", + "Indicates whether the main button is currently toggled.", + FALSE, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for control buttons. +// +void foobar_control_button_init( FoobarControlButton* self ) +{ + GtkWidget* icon = gtk_image_new( ); + gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); + g_object_bind_property( + self, + "icon-name", + icon, + "icon-name", + G_BINDING_SYNC_CREATE ); + + GtkWidget* label = gtk_label_new( NULL ); + gtk_widget_set_valign( label, GTK_ALIGN_CENTER ); + gtk_widget_set_hexpand( label, TRUE ); + gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); + gtk_label_set_xalign( GTK_LABEL( label ), 0 ); + g_object_bind_property( + self, + "label", + label, + "label", + G_BINDING_SYNC_CREATE ); + + GtkWidget* content = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); + gtk_box_append( GTK_BOX( content ), icon ); + gtk_box_append( GTK_BOX( content ), label ); + + GtkWidget* primary_button = gtk_button_new( ); + gtk_widget_add_css_class( primary_button, "primary" ); + gtk_widget_set_hexpand( primary_button, TRUE ); + gtk_button_set_child( GTK_BUTTON( primary_button ), content ); + g_object_bind_property( + self, + "can-toggle", + primary_button, + "sensitive", + G_BINDING_SYNC_CREATE ); + g_signal_connect( primary_button, "clicked", G_CALLBACK( foobar_control_button_handle_primary_clicked ), self ); + + GtkWidget* expand_button = gtk_button_new_from_icon_name( "fluent-chevron-right-symbolic" ); + gtk_widget_add_css_class( expand_button, "expand" ); + g_object_bind_property( + self, + "can-expand", + expand_button, + "visible", + G_BINDING_SYNC_CREATE ); + g_signal_connect( expand_button, "clicked", G_CALLBACK( foobar_control_button_handle_expand_clicked ), self ); + + GtkLayoutManager* layout = gtk_widget_get_layout_manager( GTK_WIDGET( self ) ); + gtk_orientable_set_orientation( GTK_ORIENTABLE( layout ), GTK_ORIENTATION_HORIZONTAL ); + gtk_widget_insert_before( primary_button, GTK_WIDGET( self ), NULL ); + gtk_widget_insert_before( expand_button, GTK_WIDGET( self ), NULL ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_control_button_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarControlButton* self = (FoobarControlButton*)object; + + switch ( prop_id ) + { + case PROP_ICON_NAME: + g_value_set_string( value, foobar_control_button_get_icon_name( self ) ); + break; + case PROP_LABEL: + g_value_set_string( value, foobar_control_button_get_label( self ) ); + break; + case PROP_CAN_EXPAND: + g_value_set_boolean( value, foobar_control_button_can_expand( self ) ); + break; + case PROP_CAN_TOGGLE: + g_value_set_boolean( value, foobar_control_button_can_toggle( self ) ); + break; + case PROP_IS_EXPANDED: + g_value_set_boolean( value, foobar_control_button_is_expanded( self ) ); + break; + case PROP_IS_TOGGLED: + g_value_set_boolean( value, foobar_control_button_is_toggled( 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_control_button_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarControlButton* self = (FoobarControlButton*)object; + + switch ( prop_id ) + { + case PROP_ICON_NAME: + foobar_control_button_set_icon_name( self, g_value_get_string( value ) ); + break; + case PROP_LABEL: + foobar_control_button_set_label( self, g_value_get_string( value ) ); + break; + case PROP_CAN_EXPAND: + foobar_control_button_set_can_expand( self, g_value_get_boolean( value ) ); + break; + case PROP_CAN_TOGGLE: + foobar_control_button_set_can_toggle( self, g_value_get_boolean( value ) ); + break; + case PROP_IS_EXPANDED: + foobar_control_button_set_expanded( self, g_value_get_boolean( value ) ); + break; + case PROP_IS_TOGGLED: + foobar_control_button_set_toggled( self, g_value_get_boolean( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for control buttons. +// +void foobar_control_button_dispose( GObject* object ) +{ + FoobarControlButton* self = (FoobarControlButton*)object; + + GtkWidget* child; + while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) + { + gtk_widget_unparent( child ); + } + + G_OBJECT_CLASS( foobar_control_button_parent_class )->dispose( object ); +} + +// +// Instance cleanup for control buttons. +// +void foobar_control_button_finalize( GObject* object ) +{ + FoobarControlButton* self = (FoobarControlButton*)object; + + g_clear_pointer( &self->icon_name, g_free ); + g_clear_pointer( &self->label, g_free ); + + G_OBJECT_CLASS( foobar_control_button_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new control button instance. +// +GtkWidget* foobar_control_button_new( void ) +{ + return g_object_new( FOOBAR_TYPE_CONTROL_BUTTON, NULL ); +} + +// +// Get the name of the icon to be displayed next to the label. +// +gchar const* foobar_control_button_get_icon_name( FoobarControlButton* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), NULL ); + return self->icon_name; +} + +// +// Get the button's main label. +// +gchar const* foobar_control_button_get_label( FoobarControlButton* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), NULL ); + return self->label; +} + +// +// Check whether some details are hidden and can be revealed by a toggle button next to the main action. +// +gboolean foobar_control_button_can_expand( FoobarControlButton* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE ); + return self->can_expand; +} + +// +// Check whether the main button can be toggled. +// +gboolean foobar_control_button_can_toggle( FoobarControlButton* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE ); + return self->can_toggle; +} + +// +// Check whether additional details should currently be shown. +// +gboolean foobar_control_button_is_expanded( FoobarControlButton* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE ); + return self->is_expanded; +} + +// +// Check whether the main button is currently toggled. +// +gboolean foobar_control_button_is_toggled( FoobarControlButton* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE ); + return self->is_toggled; +} + +// +// Update the name of the icon to be displayed next to the label. +// +void foobar_control_button_set_icon_name( + FoobarControlButton* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); + + if ( g_strcmp0( self->icon_name, value ) ) + { + g_clear_pointer( &self->icon_name, g_free ); + self->icon_name = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ICON_NAME] ); + } +} + +// +// Update the button's main label. +// +void foobar_control_button_set_label( + FoobarControlButton* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); + + if ( g_strcmp0( self->label, value ) ) + { + g_clear_pointer( &self->label, g_free ); + self->label = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] ); + } +} + +// +// Update whether some details are hidden and can be revealed by a toggle button next to the main action. +// +void foobar_control_button_set_can_expand( + FoobarControlButton* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); + + if ( !value ) { foobar_control_button_set_expanded( self, FALSE ); } + + value = !!value; + if ( self->can_expand != value ) + { + self->can_expand = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_EXPAND] ); + } +} + +// +// Update whether the main button can be toggled. +// +void foobar_control_button_set_can_toggle( + FoobarControlButton* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); + + if ( !value ) { foobar_control_button_set_toggled( self, FALSE ); } + + value = !!value; + if ( self->can_toggle != value ) + { + self->can_toggle = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_TOGGLE] ); + } +} + +// +// Update whether additional details are currently shown. +// +void foobar_control_button_set_expanded( + FoobarControlButton* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); + + value = value && self->can_expand; + if ( self->is_expanded != value ) + { + self->is_expanded = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] ); + + if ( self->is_expanded ) + { + gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" ); + } + else + { + gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" ); + } + } +} + +// +// Update whether the main button is currently toggled. +// +void foobar_control_button_set_toggled( + FoobarControlButton* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); + + value = value && self->can_toggle; + if ( self->is_toggled != value ) + { + self->is_toggled = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_TOGGLED] ); + + if ( self->is_toggled ) + { + gtk_widget_add_css_class( GTK_WIDGET( self ), "toggled" ); + } + else + { + gtk_widget_remove_css_class( GTK_WIDGET( self ), "toggled" ); + } + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called when the primary button was clicked. +// +// If allowed, this will toggle the button. +// +void foobar_control_button_handle_primary_clicked( GtkButton* button, gpointer userdata ) +{ + (void)button; + FoobarControlButton* self = (FoobarControlButton*)userdata; + + if ( foobar_control_button_can_toggle( self ) ) + { + foobar_control_button_set_toggled( self, !foobar_control_button_is_toggled( self ) ); + } +} + +// +// Called when the expand button was clicked. +// +// If allowed, this will toggle the "expanded" state of the button. +// +void foobar_control_button_handle_expand_clicked( GtkButton* button, gpointer userdata ) +{ + (void)button; + FoobarControlButton* self = (FoobarControlButton*)userdata; + + if ( foobar_control_button_can_expand( self ) ) + { + foobar_control_button_set_expanded( self, !foobar_control_button_is_expanded( self ) ); + } +}
\ No newline at end of file diff --git a/src/widgets/control-center/control-button.h b/src/widgets/control-center/control-button.h new file mode 100644 index 0000000..48dcfb1 --- /dev/null +++ b/src/widgets/control-center/control-button.h @@ -0,0 +1,31 @@ +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_CONTROL_BUTTON foobar_control_button_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarControlButton, foobar_control_button, FOOBAR, CONTROL_BUTTON, GtkWidget ) + +GtkWidget* foobar_control_button_new ( void ); +gchar const* foobar_control_button_get_icon_name ( FoobarControlButton* self ); +gchar const* foobar_control_button_get_label ( FoobarControlButton* self ); +gboolean foobar_control_button_can_expand ( FoobarControlButton* self ); +gboolean foobar_control_button_can_toggle ( FoobarControlButton* self ); +gboolean foobar_control_button_is_expanded ( FoobarControlButton* self ); +gboolean foobar_control_button_is_toggled ( FoobarControlButton* self ); +void foobar_control_button_set_icon_name ( FoobarControlButton* self, + gchar const* value ); +void foobar_control_button_set_label ( FoobarControlButton* self, + gchar const* value ); +void foobar_control_button_set_can_expand( FoobarControlButton* self, + gboolean value ); +void foobar_control_button_set_can_toggle( FoobarControlButton* self, + gboolean value ); +void foobar_control_button_set_expanded ( FoobarControlButton* self, + gboolean value ); +void foobar_control_button_set_toggled ( FoobarControlButton* self, + gboolean value ); + +G_END_DECLS
\ No newline at end of file diff --git a/src/widgets/control-center/control-details-item.c b/src/widgets/control-center/control-details-item.c new file mode 100644 index 0000000..f8858d5 --- /dev/null +++ b/src/widgets/control-center/control-details-item.c @@ -0,0 +1,307 @@ +#include "widgets/control-center/control-details-item.h" + +// +// FoobarControlDetailsAccessory: +// +// The accessory shown next to an item in the control center details list (if any). +// + +G_DEFINE_ENUM_TYPE( + FoobarControlDetailsAccessory, + foobar_control_details_accessory, + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE, "none" ), + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS, "progress" ), + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED, "checked" ) ) + +// +// FoobarControlDetailsItem: +// +// Standard row layout for items in a FoobarControlDetails list. The rows are made up of a label and an accessory that +// is shown or hidden depending on the item's state. +// + +struct _FoobarControlDetailsItem +{ + GtkWidget parent_instance; + GtkWidget* spinner; + GtkWidget* checkmark; + gchar* label; + FoobarControlDetailsAccessory accessory; +}; + +enum +{ + PROP_LABEL = 1, + PROP_ACCESSORY, + PROP_IS_CHECKED, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_control_details_item_class_init ( FoobarControlDetailsItemClass* klass ); +static void foobar_control_details_item_init ( FoobarControlDetailsItem* self ); +static void foobar_control_details_item_get_property( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_control_details_item_set_property( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_control_details_item_dispose ( GObject* object ); +static void foobar_control_details_item_finalize ( GObject* object ); + +G_DEFINE_FINAL_TYPE( FoobarControlDetailsItem, foobar_control_details_item, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for control details items. +// +void foobar_control_details_item_class_init( FoobarControlDetailsItemClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + gtk_widget_class_set_css_name( widget_klass, "control-details-item" ); + gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_control_details_item_get_property; + object_klass->set_property = foobar_control_details_item_set_property; + object_klass->dispose = foobar_control_details_item_dispose; + object_klass->finalize = foobar_control_details_item_finalize; + + props[PROP_LABEL] = g_param_spec_string( + "label", + "Label", + "Label shown for the item.", + NULL, + G_PARAM_READWRITE ); + props[PROP_ACCESSORY] = g_param_spec_enum( + "accessory", + "Accessory", + "Accessory shown for the item.", + FOOBAR_TYPE_CONTROL_DETAILS_ACCESSORY, + 0, + G_PARAM_READWRITE ); + props[PROP_IS_CHECKED] = g_param_spec_boolean( + "is-checked", + "Is Checked", + "Current checked-state for the item.", + FALSE, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for control details items. +// +void foobar_control_details_item_init( FoobarControlDetailsItem* self ) +{ + GtkWidget* label = gtk_label_new( NULL ); + gtk_widget_set_hexpand( label, TRUE ); + gtk_widget_set_valign( label, GTK_ALIGN_CENTER ); + gtk_label_set_ellipsize( GTK_LABEL( label ), PANGO_ELLIPSIZE_END ); + gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); + gtk_label_set_xalign( GTK_LABEL( label ), 0 ); + gtk_label_set_wrap( GTK_LABEL( label ), FALSE ); + g_object_bind_property( + self, + "label", + label, + "label", + G_BINDING_SYNC_CREATE ); + + self->spinner = gtk_spinner_new( ); + gtk_spinner_set_spinning( GTK_SPINNER( self->spinner ), TRUE ); + gtk_widget_add_css_class( self->spinner, "accessory" ); + gtk_widget_set_valign( self->spinner, GTK_ALIGN_CENTER ); + gtk_widget_set_visible( self->spinner, FALSE ); + + self->checkmark = gtk_image_new_from_icon_name( "fluent-checkmark-symbolic" ); + gtk_widget_add_css_class( self->checkmark, "checkmark" ); + gtk_widget_add_css_class( self->checkmark, "accessory" ); + gtk_widget_set_valign( self->checkmark, GTK_ALIGN_CENTER ); + gtk_widget_set_visible( self->checkmark, FALSE ); + + gtk_widget_insert_before( label, GTK_WIDGET( self ), NULL ); + gtk_widget_insert_before( self->spinner, GTK_WIDGET( self ), NULL ); + gtk_widget_insert_before( self->checkmark, GTK_WIDGET( self ), NULL ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_control_details_item_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object; + + switch ( prop_id ) + { + case PROP_LABEL: + g_value_set_string( value, foobar_control_details_item_get_label( self ) ); + break; + case PROP_ACCESSORY: + g_value_set_enum( value, foobar_control_details_item_get_accessory( self ) ); + break; + case PROP_IS_CHECKED: + g_value_set_boolean( value, foobar_control_details_item_is_checked( 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_control_details_item_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object; + + switch ( prop_id ) + { + case PROP_LABEL: + foobar_control_details_item_set_label( self, g_value_get_string( value ) ); + break; + case PROP_ACCESSORY: + foobar_control_details_item_set_accessory( self, g_value_get_enum( value ) ); + break; + case PROP_IS_CHECKED: + foobar_control_details_item_set_checked( self, g_value_get_boolean( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for control details items. +// +void foobar_control_details_item_dispose( GObject* object ) +{ + FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object; + + GtkWidget* child; + while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) + { + gtk_widget_unparent( child ); + } + + G_OBJECT_CLASS( foobar_control_details_item_parent_class )->dispose( object ); +} + +// +// Instance cleanup for control details items. +// +void foobar_control_details_item_finalize( GObject* object ) +{ + FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object; + + g_clear_pointer( &self->label, g_free ); + + G_OBJECT_CLASS( foobar_control_details_item_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new control details item instance. +// +GtkWidget* foobar_control_details_item_new( void ) +{ + return g_object_new( FOOBAR_TYPE_CONTROL_DETAILS_ITEM, NULL ); +} + +// +// Get the label shown for the item. +// +gchar const* foobar_control_details_item_get_label( FoobarControlDetailsItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), NULL ); + return self->label; +} + +// +// Get the accessory shown for the item. +// +FoobarControlDetailsAccessory foobar_control_details_item_get_accessory( FoobarControlDetailsItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), 0 ); + return self->accessory; +} + +// +// Get the current checked-state for the item. +// +gboolean foobar_control_details_item_is_checked( FoobarControlDetailsItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), FALSE ); + return self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED; +} + +// +// Update the label shown for the item. +// +void foobar_control_details_item_set_label( + FoobarControlDetailsItem* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) ); + + if ( g_strcmp0( self->label, value ) ) + { + g_clear_pointer( &self->label, g_free ); + self->label = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] ); + } +} + +// +// Update the current accessory shown for the item. +// +void foobar_control_details_item_set_accessory( + FoobarControlDetailsItem* self, + FoobarControlDetailsAccessory value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) ); + + if ( self->accessory != value ) + { + self->accessory = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ACCESSORY] ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_CHECKED] ); + + gtk_widget_set_visible( self->spinner, self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS ); + gtk_widget_set_visible( self->checkmark, self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED ); + } +} + +// +// Update the current checked-state for the item. +// +void foobar_control_details_item_set_checked( + FoobarControlDetailsItem* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) ); + + FoobarControlDetailsAccessory accessory = value + ? FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED + : FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE; + foobar_control_details_item_set_accessory( self, accessory ); +}
\ No newline at end of file diff --git a/src/widgets/control-center/control-details-item.h b/src/widgets/control-center/control-details-item.h new file mode 100644 index 0000000..b9be4a7 --- /dev/null +++ b/src/widgets/control-center/control-details-item.h @@ -0,0 +1,32 @@ +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_CONTROL_DETAILS_ACCESSORY foobar_control_details_accessory_get_type( ) +#define FOOBAR_TYPE_CONTROL_DETAILS_ITEM foobar_control_details_item_get_type( ) + +typedef enum +{ + FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE, + FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED, + FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS, +} FoobarControlDetailsAccessory; + +GType foobar_control_details_accessory_get_type( void ); + +G_DECLARE_FINAL_TYPE( FoobarControlDetailsItem, foobar_control_details_item, FOOBAR, CONTROL_DETAILS_ITEM, GtkWidget ) + +GtkWidget* foobar_control_details_item_new ( void ); +gchar const* foobar_control_details_item_get_label ( FoobarControlDetailsItem* self ); +FoobarControlDetailsAccessory foobar_control_details_item_get_accessory( FoobarControlDetailsItem* self ); +gboolean foobar_control_details_item_is_checked ( FoobarControlDetailsItem* self ); +void foobar_control_details_item_set_label ( FoobarControlDetailsItem* self, + gchar const* value ); +void foobar_control_details_item_set_accessory( FoobarControlDetailsItem* self, + FoobarControlDetailsAccessory value ); +void foobar_control_details_item_set_checked ( FoobarControlDetailsItem* self, + gboolean value ); + +G_END_DECLS
\ No newline at end of file diff --git a/src/widgets/control-center/control-details.c b/src/widgets/control-center/control-details.c new file mode 100644 index 0000000..4f0d422 --- /dev/null +++ b/src/widgets/control-center/control-details.c @@ -0,0 +1,289 @@ +#include "widgets/control-center/control-details.h" + +// +// FoobarControlDetails: +// +// A revealer for details in the "controls" section of the control center. This widget is usually used to embed a list +// view. It takes care of scrolling by embedding the child in a GtkScrolledWindow. +// + +struct _FoobarControlDetails +{ + GtkWidget parent_instance; + GtkWidget* child; + gboolean is_expanded; + gint inset_top; +}; + +enum +{ + PROP_CHILD = 1, + PROP_IS_EXPANDED, + PROP_INSET_TOP, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_control_details_class_init ( FoobarControlDetailsClass* klass ); +static void foobar_control_details_init ( FoobarControlDetails* self ); +static void foobar_control_details_get_property( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_control_details_set_property( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_control_details_dispose ( GObject* object ); + +G_DEFINE_FINAL_TYPE( FoobarControlDetails, foobar_control_details, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for control details. +// +void foobar_control_details_class_init( FoobarControlDetailsClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + gtk_widget_class_set_css_name( widget_klass, "control-details" ); + gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BIN_LAYOUT ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_control_details_get_property; + object_klass->set_property = foobar_control_details_set_property; + object_klass->dispose = foobar_control_details_dispose; + + props[PROP_CHILD] = g_param_spec_object( + "child", + "Child", + "Child widget for the details revealer.", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE ); + props[PROP_IS_EXPANDED] = g_param_spec_boolean( + "is-expanded", + "Is Expanded", + "Current state of the details revealer.", + FALSE, + G_PARAM_READWRITE ); + props[PROP_INSET_TOP] = g_param_spec_int( + "inset-top", + "Inset Top", + "Inset from the top edge (only visible when expanded).", + 0, + INT_MAX, + 0, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for control details. +// +void foobar_control_details_init( FoobarControlDetails* self ) +{ + GtkWidget* scrolled_window = gtk_scrolled_window_new( ); + gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scrolled_window ), GTK_POLICY_NEVER, GTK_POLICY_EXTERNAL ); + gtk_scrolled_window_set_propagate_natural_height( GTK_SCROLLED_WINDOW( scrolled_window ), TRUE ); + g_object_bind_property( + self, + "child", + scrolled_window, + "child", + G_BINDING_SYNC_CREATE ); + g_object_bind_property( + self, + "inset-top", + scrolled_window, + "margin-top", + G_BINDING_SYNC_CREATE ); + + GtkWidget* revealer = gtk_revealer_new( ); + gtk_revealer_set_transition_type( GTK_REVEALER( revealer ), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN ); + gtk_revealer_set_transition_duration( GTK_REVEALER( revealer ), 100 ); + gtk_revealer_set_child( GTK_REVEALER( revealer ), scrolled_window ); + g_object_bind_property( + self, + "is-expanded", + revealer, + "reveal-child", + G_BINDING_SYNC_CREATE ); + + gtk_widget_set_parent( revealer, GTK_WIDGET( self ) ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_control_details_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarControlDetails* self = (FoobarControlDetails*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + g_value_set_object( value, foobar_control_details_get_child( self ) ); + break; + case PROP_IS_EXPANDED: + g_value_set_boolean( value, foobar_control_details_is_expanded( self ) ); + break; + case PROP_INSET_TOP: + g_value_set_int( value, foobar_control_details_get_inset_top( 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_control_details_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarControlDetails* self = (FoobarControlDetails*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + foobar_control_details_set_child( self, g_value_get_object( value ) ); + break; + case PROP_IS_EXPANDED: + foobar_control_details_set_expanded( self, g_value_get_boolean( value ) ); + break; + case PROP_INSET_TOP: + foobar_control_details_set_inset_top( self, g_value_get_int( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for control details. +// +void foobar_control_details_dispose( GObject* object ) +{ + FoobarControlDetails* self = (FoobarControlDetails*)object; + + GtkWidget* child; + while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) + { + gtk_widget_unparent( child ); + } + + g_clear_object( &self->child ); + + G_OBJECT_CLASS( foobar_control_details_parent_class )->dispose( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new control details instance. +// +GtkWidget* foobar_control_details_new( void ) +{ + return g_object_new( FOOBAR_TYPE_CONTROL_DETAILS, NULL ); +} + +// +// Get the child widget for the details revealer. +// +GtkWidget* foobar_control_details_get_child( FoobarControlDetails* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), NULL ); + return self->child; +} + +// +// Get the current state of the details revealer. +// +gboolean foobar_control_details_is_expanded( FoobarControlDetails* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), FALSE ); + return self->is_expanded; +} + +// +// Get the inset from the top edge (only visible when expanded). +// +gint foobar_control_details_get_inset_top( FoobarControlDetails* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), FALSE ); + return self->inset_top; +} + +// +// Update the child widget for the details revealer. +// +void foobar_control_details_set_child( + FoobarControlDetails* self, + GtkWidget* value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) ); + + if ( self->child != value ) + { + g_clear_object( &self->child ); + if ( value ) { self->child = g_object_ref( value ); } + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] ); + } +} + +// +// Update the current state of the details revealer. +// +void foobar_control_details_set_expanded( + FoobarControlDetails* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) ); + + value = !!value; + if ( self->is_expanded != value ) + { + self->is_expanded = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] ); + + if ( self->is_expanded ) + { + gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" ); + } + else + { + gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" ); + } + } +} + +// +// Update the inset from the top edge (only visible when expanded). +// +void foobar_control_details_set_inset_top( + FoobarControlDetails* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( 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] ); + } +}
\ No newline at end of file diff --git a/src/widgets/control-center/control-details.h b/src/widgets/control-center/control-details.h new file mode 100644 index 0000000..71df683 --- /dev/null +++ b/src/widgets/control-center/control-details.h @@ -0,0 +1,22 @@ +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_CONTROL_DETAILS foobar_control_details_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarControlDetails, foobar_control_details, FOOBAR, CONTROL_DETAILS, GtkWidget ) + +GtkWidget* foobar_control_details_new ( void ); +GtkWidget* foobar_control_details_get_child ( FoobarControlDetails* self ); +gboolean foobar_control_details_is_expanded ( FoobarControlDetails* self ); +gint foobar_control_details_get_inset_top( FoobarControlDetails* self ); +void foobar_control_details_set_child ( FoobarControlDetails* self, + GtkWidget* value ); +void foobar_control_details_set_expanded ( FoobarControlDetails* self, + gboolean value ); +void foobar_control_details_set_inset_top( FoobarControlDetails* self, + gint value ); + +G_END_DECLS
\ No newline at end of file diff --git a/src/widgets/control-center/control-slider.c b/src/widgets/control-center/control-slider.c new file mode 100644 index 0000000..d98dc9e --- /dev/null +++ b/src/widgets/control-center/control-slider.c @@ -0,0 +1,428 @@ +#include "widgets/control-center/control-slider.h" + +// +// FoobarControlSlider: +// +// A slider in the "controls" section of the control center with a title and icon, as well as an optional secondary +// button to toggle a details view. +// + +struct _FoobarControlSlider +{ + GtkWidget parent_instance; + gchar* icon_name; + gchar* label; + gboolean can_expand; + gboolean is_expanded; + gint percentage; +}; + +enum +{ + PROP_ICON_NAME = 1, + PROP_LABEL, + PROP_CAN_EXPAND, + PROP_IS_EXPANDED, + PROP_PERCENTAGE, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_control_slider_class_init ( FoobarControlSliderClass* klass ); +static void foobar_control_slider_init ( FoobarControlSlider* self ); +static void foobar_control_slider_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_control_slider_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_control_slider_dispose ( GObject* object ); +static void foobar_control_slider_finalize ( GObject* object ); +static void foobar_control_slider_handle_expand_clicked( GtkButton* button, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarControlSlider, foobar_control_slider, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for control sliders. +// +void foobar_control_slider_class_init( FoobarControlSliderClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + gtk_widget_class_set_css_name( widget_klass, "control-slider" ); + gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_control_slider_get_property; + object_klass->set_property = foobar_control_slider_set_property; + object_klass->dispose = foobar_control_slider_dispose; + object_klass->finalize = foobar_control_slider_finalize; + + props[PROP_ICON_NAME] = g_param_spec_string( + "icon-name", + "Icon Name", + "Name of the icon to be displayed next to the label.", + NULL, + G_PARAM_READWRITE ); + props[PROP_LABEL] = g_param_spec_string( + "label", + "Label", + "The slider's main label.", + NULL, + G_PARAM_READWRITE ); + props[PROP_CAN_EXPAND] = g_param_spec_boolean( + "can-expand", + "Can Expand", + "Indicates whether some details are hidden and can be revealed by a toggle button next to the slider.", + FALSE, + G_PARAM_READWRITE ); + props[PROP_IS_EXPANDED] = g_param_spec_boolean( + "is-expanded", + "Is Expanded", + "Indicates whether additional details should be shown.", + FALSE, + G_PARAM_READWRITE ); + props[PROP_PERCENTAGE] = g_param_spec_int( + "percentage", + "Percentage", + "Value shown by the slider.", + 0, + 100, + 0, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for control sliders. +// +void foobar_control_slider_init( FoobarControlSlider* self ) +{ + GtkWidget* icon = gtk_image_new( ); + gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); + g_object_bind_property( + self, + "icon-name", + icon, + "icon-name", + G_BINDING_SYNC_CREATE ); + + GtkWidget* label = gtk_label_new( NULL ); + gtk_widget_set_valign( label, GTK_ALIGN_CENTER ); + gtk_widget_set_hexpand( label, TRUE ); + gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); + gtk_label_set_xalign( GTK_LABEL( label ), 0 ); + g_object_bind_property( + self, + "label", + label, + "label", + G_BINDING_SYNC_CREATE ); + + GtkWidget* header = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); + gtk_box_append( GTK_BOX( header ), icon ); + gtk_box_append( GTK_BOX( header ), label ); + + GtkWidget* scale = gtk_scale_new_with_range( GTK_ORIENTATION_HORIZONTAL, 0, 100, 2 ); + g_object_bind_property( + self, + "percentage", + gtk_range_get_adjustment( GTK_RANGE( scale ) ), + "value", + G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL ); + + GtkWidget* primary_content = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); + gtk_widget_add_css_class( primary_content, "primary" ); + gtk_box_append( GTK_BOX( primary_content ), header ); + gtk_box_append( GTK_BOX( primary_content ), scale ); + + GtkWidget* expand_button = gtk_button_new_from_icon_name( "fluent-chevron-right-symbolic" ); + gtk_widget_add_css_class( expand_button, "expand" ); + g_object_bind_property( + self, + "can-expand", + expand_button, + "visible", + G_BINDING_SYNC_CREATE ); + g_signal_connect( expand_button, "clicked", G_CALLBACK( foobar_control_slider_handle_expand_clicked ), self ); + + GtkLayoutManager* layout = gtk_widget_get_layout_manager( GTK_WIDGET( self ) ); + gtk_orientable_set_orientation( GTK_ORIENTABLE( layout ), GTK_ORIENTATION_HORIZONTAL ); + gtk_widget_insert_before( primary_content, GTK_WIDGET( self ), NULL ); + gtk_widget_insert_before( expand_button, GTK_WIDGET( self ), NULL ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_control_slider_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarControlSlider* self = (FoobarControlSlider*)object; + + switch ( prop_id ) + { + case PROP_ICON_NAME: + g_value_set_string( value, foobar_control_slider_get_icon_name( self ) ); + break; + case PROP_LABEL: + g_value_set_string( value, foobar_control_slider_get_label( self ) ); + break; + case PROP_CAN_EXPAND: + g_value_set_boolean( value, foobar_control_slider_can_expand( self ) ); + break; + case PROP_IS_EXPANDED: + g_value_set_boolean( value, foobar_control_slider_is_expanded( self ) ); + break; + case PROP_PERCENTAGE: + g_value_set_int( value, foobar_control_slider_get_percentage( 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_control_slider_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarControlSlider* self = (FoobarControlSlider*)object; + + switch ( prop_id ) + { + case PROP_ICON_NAME: + foobar_control_slider_set_icon_name( self, g_value_get_string( value ) ); + break; + case PROP_LABEL: + foobar_control_slider_set_label( self, g_value_get_string( value ) ); + break; + case PROP_CAN_EXPAND: + foobar_control_slider_set_can_expand( self, g_value_get_boolean( value ) ); + break; + case PROP_IS_EXPANDED: + foobar_control_slider_set_expanded( self, g_value_get_boolean( value ) ); + break; + case PROP_PERCENTAGE: + foobar_control_slider_set_percentage( self, g_value_get_int( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for control sliders. +// +void foobar_control_slider_dispose( GObject* object ) +{ + FoobarControlSlider* self = (FoobarControlSlider*)object; + + GtkWidget* child; + while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) + { + gtk_widget_unparent( child ); + } + + G_OBJECT_CLASS( foobar_control_slider_parent_class )->dispose( object ); +} + +// +// Instance cleanup for control sliders. +// +void foobar_control_slider_finalize( GObject* object ) +{ + FoobarControlSlider* self = (FoobarControlSlider*)object; + + g_clear_pointer( &self->icon_name, g_free ); + g_clear_pointer( &self->label, g_free ); + + G_OBJECT_CLASS( foobar_control_slider_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new control slider instance. +// +GtkWidget* foobar_control_slider_new( void ) +{ + return g_object_new( FOOBAR_TYPE_CONTROL_SLIDER, NULL ); +} + +// +// Get the name of the icon to be displayed next to the label. +// +gchar const* foobar_control_slider_get_icon_name( FoobarControlSlider* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), NULL ); + return self->icon_name; +} + +// +// Get the slider's main label. +// +gchar const* foobar_control_slider_get_label( FoobarControlSlider* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), NULL ); + return self->label; +} + +// +// Check whether some details are hidden and can be revealed by a toggle button next to the slider. +// +gboolean foobar_control_slider_can_expand( FoobarControlSlider* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE ); + return self->can_expand; +} + +// +// Check whether additional details should currently be shown. +// +gboolean foobar_control_slider_is_expanded( FoobarControlSlider* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE ); + return self->is_expanded; +} + +// +// Get the value shown by the slider. +// +gint foobar_control_slider_get_percentage( FoobarControlSlider* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE ); + return self->percentage; +} + +// +// Update the name of the icon to be displayed next to the label. +// +void foobar_control_slider_set_icon_name( + FoobarControlSlider* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); + + if ( g_strcmp0( self->icon_name, value ) ) + { + g_clear_pointer( &self->icon_name, g_free ); + self->icon_name = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ICON_NAME] ); + } +} + +// +// Update the slider's main label. +// +void foobar_control_slider_set_label( + FoobarControlSlider* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); + + if ( g_strcmp0( self->label, value ) ) + { + g_clear_pointer( &self->label, g_free ); + self->label = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] ); + } +} + +// +// Update whether some details are hidden and can be revealed by a toggle button next to the slider. +// +void foobar_control_slider_set_can_expand( + FoobarControlSlider* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); + + if ( !value ) { foobar_control_slider_set_expanded( self, FALSE ); } + + value = !!value; + if ( self->can_expand != value ) + { + self->can_expand = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_EXPAND] ); + } +} + +// +// Update whether additional details are currently shown. +// +void foobar_control_slider_set_expanded( + FoobarControlSlider* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); + + value = value && self->can_expand; + if ( self->is_expanded != value ) + { + self->is_expanded = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] ); + + if ( self->is_expanded ) + { + gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" ); + } + else + { + gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" ); + } + } +} + +// +// Update the value shown by the slider. +// +void foobar_control_slider_set_percentage( FoobarControlSlider* self, gint value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); + + value = CLAMP( value, 0, 100 ); + if ( self->percentage != value ) + { + self->percentage = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_PERCENTAGE] ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called when the expand button was clicked. +// +// If allowed, this will toggle the "expanded" state of the slider. +// +void foobar_control_slider_handle_expand_clicked( GtkButton* button, gpointer userdata ) +{ + (void)button; + FoobarControlSlider* self = (FoobarControlSlider*)userdata; + + if ( foobar_control_slider_can_expand( self ) ) + { + foobar_control_slider_set_expanded( self, !foobar_control_slider_is_expanded( self ) ); + } +}
\ No newline at end of file diff --git a/src/widgets/control-center/control-slider.h b/src/widgets/control-center/control-slider.h new file mode 100644 index 0000000..e7dfac0 --- /dev/null +++ b/src/widgets/control-center/control-slider.h @@ -0,0 +1,28 @@ +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_CONTROL_SLIDER foobar_control_slider_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarControlSlider, foobar_control_slider, FOOBAR, CONTROL_SLIDER, GtkWidget ) + +GtkWidget* foobar_control_slider_new ( void ); +gchar const* foobar_control_slider_get_icon_name ( FoobarControlSlider* self ); +gchar const* foobar_control_slider_get_label ( FoobarControlSlider* self ); +gboolean foobar_control_slider_can_expand ( FoobarControlSlider* self ); +gboolean foobar_control_slider_is_expanded ( FoobarControlSlider* self ); +gint foobar_control_slider_get_percentage( FoobarControlSlider* self ); +void foobar_control_slider_set_icon_name ( FoobarControlSlider* self, + gchar const* value ); +void foobar_control_slider_set_label ( FoobarControlSlider* self, + gchar const* value ); +void foobar_control_slider_set_can_expand( FoobarControlSlider* self, + gboolean value ); +void foobar_control_slider_set_expanded ( FoobarControlSlider* self, + gboolean value ); +void foobar_control_slider_set_percentage( FoobarControlSlider* self, + gint value ); + +G_END_DECLS
\ No newline at end of file diff --git a/src/widgets/control-center/meson.build b/src/widgets/control-center/meson.build new file mode 100644 index 0000000..5b1252d --- /dev/null +++ b/src/widgets/control-center/meson.build @@ -0,0 +1,6 @@ +foobar_sources += files( + 'control-button.c', + 'control-slider.c', + 'control-details.c', + 'control-details-item.c', +)
\ No newline at end of file diff --git a/src/widgets/inset-container.c b/src/widgets/inset-container.c new file mode 100644 index 0000000..9b9b132 --- /dev/null +++ b/src/widgets/inset-container.c @@ -0,0 +1,363 @@ +#include "widgets/inset-container.h" + +// +// FoobarInsetContainer: +// +// A custom widget which renders a child with an inset/padding. +// +// Notably, it also supports negative inset values which is useful in order to provide uniform spacing between widgets. +// + +struct _FoobarInsetContainer +{ + GtkWidget parent_instance; + GtkWidget* child; + gint inset_horizontal; + gint inset_vertical; +}; + +enum +{ + PROP_CHILD = 1, + PROP_INSET_HORIZONTAL, + PROP_INSET_VERTICAL, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_inset_container_class_init ( FoobarInsetContainerClass* klass ); +static void foobar_inset_container_init ( FoobarInsetContainer* self ); +static void foobar_inset_container_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_inset_container_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_inset_container_dispose ( GObject* object ); +static GtkSizeRequestMode foobar_inset_container_get_request_mode( GtkWidget* widget ); +static void foobar_inset_container_measure ( GtkWidget* widget, + GtkOrientation orientation, + int for_size, + int* minimum, + int* natural, + int* minimum_baseline, + int* natural_baseline ); +static void foobar_inset_container_size_allocate ( GtkWidget* widget, + int width, + int height, + int baseline ); + +G_DEFINE_FINAL_TYPE( FoobarInsetContainer, foobar_inset_container, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for inset containers. +// +void foobar_inset_container_class_init( FoobarInsetContainerClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + widget_klass->get_request_mode = foobar_inset_container_get_request_mode; + widget_klass->measure = foobar_inset_container_measure; + widget_klass->size_allocate = foobar_inset_container_size_allocate; + gtk_widget_class_set_css_name( widget_klass, "inset-container" ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_inset_container_get_property; + object_klass->set_property = foobar_inset_container_set_property; + object_klass->dispose = foobar_inset_container_dispose; + + props[PROP_CHILD] = g_param_spec_object( + "child", + "Child", + "Child widget for the panel item.", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE ); + props[PROP_INSET_HORIZONTAL] = g_param_spec_int( + "inset-horizontal", + "Inset Horizontal", + "The horizontal offset (may be negative).", + INT_MIN, + INT_MAX, + 0, + G_PARAM_READWRITE ); + props[PROP_INSET_VERTICAL] = g_param_spec_int( + "inset-vertical", + "Inset Vertical", + "The vertical offset (may be negative).", + INT_MIN, + INT_MAX, + 0, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for inset containers. +// +void foobar_inset_container_init( FoobarInsetContainer* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_inset_container_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarInsetContainer* self = (FoobarInsetContainer*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + g_value_set_object( value, foobar_inset_container_get_child( self ) ); + break; + case PROP_INSET_HORIZONTAL: + g_value_set_int( value, foobar_inset_container_get_inset_horizontal( self ) ); + break; + case PROP_INSET_VERTICAL: + g_value_set_int( value, foobar_inset_container_get_inset_vertical( 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_inset_container_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarInsetContainer* self = (FoobarInsetContainer*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + foobar_inset_container_set_child( self, g_value_get_object( value ) ); + break; + case PROP_INSET_HORIZONTAL: + foobar_inset_container_set_inset_horizontal( self, g_value_get_int( value ) ); + break; + case PROP_INSET_VERTICAL: + foobar_inset_container_set_inset_vertical( self, g_value_get_int( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for inset containers. +// +void foobar_inset_container_dispose( GObject* object ) +{ + FoobarInsetContainer* self = (FoobarInsetContainer*)object; + + g_clear_pointer( &self->child, gtk_widget_unparent ); + + G_OBJECT_CLASS( foobar_inset_container_parent_class )->dispose( object ); +} + +// +// Determine the size request mode used to measure the widget. +// +// This implementation just forwards the request mode from the child widget. +// +GtkSizeRequestMode foobar_inset_container_get_request_mode( GtkWidget* widget ) +{ + FoobarInsetContainer* self = (FoobarInsetContainer*)widget; + if ( !self->child ) { return GTK_SIZE_REQUEST_CONSTANT_SIZE; } + + return gtk_widget_get_request_mode( self->child ); +} + +// +// Compute the requested size of the widget. +// +// This implementation uses the size requested by the child widget and then adds the inset to it. +// +void foobar_inset_container_measure( + GtkWidget* widget, + GtkOrientation orientation, + int for_size, + int* minimum, + int* natural, + int* minimum_baseline, + int* natural_baseline ) +{ + FoobarInsetContainer* self = (FoobarInsetContainer*)widget; + if ( !self->child ) + { + *minimum = 0; + *natural = 0; + *minimum_baseline = 0; + *natural_baseline = 0; + return; + } + + // Get the inset for the given orientation and the other one. + + gint inset; + gint other_inset; + switch ( orientation ) + { + case GTK_ORIENTATION_HORIZONTAL: + inset = self->inset_horizontal; + other_inset = self->inset_vertical; + break; + case GTK_ORIENTATION_VERTICAL: + inset = self->inset_vertical; + other_inset = self->inset_horizontal; + break; + default: + g_warn_if_reached( ); + return; + } + + // Compute the new size, taking into consideration that the inset might have already been added to measure the other + // side and needs to be subtracted before passing it to the child widget's measure function. + + for_size = MAX( 0, for_size - ( 2 * other_inset ) ); + gtk_widget_measure( self->child, orientation, for_size, minimum, natural, minimum_baseline, natural_baseline ); + *minimum = MAX( 0, *minimum + ( 2 * inset ) ); + *natural = MAX( 0, *natural + ( 2 * inset ) ); + if ( orientation == GTK_ORIENTATION_VERTICAL ) + { + *minimum_baseline = MAX( 0, *minimum_baseline + inset ); + *natural_baseline = MAX( 0, *natural_baseline + inset ); + } +} + +// +// Arrange the child widget. +// +void foobar_inset_container_size_allocate( + GtkWidget* widget, + int width, + int height, + int baseline ) +{ + FoobarInsetContainer* self = (FoobarInsetContainer*)widget; + if ( !self->child ) { return; } + + GtkAllocation allocation = + { + .width = MAX( 0, width - 2 * self->inset_horizontal ), + .height = MAX( 0, height - 2 * self->inset_vertical ), + .x = self->inset_horizontal, + .y = self->inset_vertical, + }; + gtk_widget_size_allocate( self->child, &allocation, MAX( 0, baseline + self->inset_vertical ) ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new inset container instance. +// +GtkWidget* foobar_inset_container_new( void ) +{ + return g_object_new( FOOBAR_TYPE_INSET_CONTAINER, NULL ); +} + +// +// Get the container's child widget. +// +GtkWidget* foobar_inset_container_get_child( FoobarInsetContainer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_INSET_CONTAINER( self ), NULL ); + return self->child; +} + +// +// Get the inset applied to the left and right side of the widget. +// +gint foobar_inset_container_get_inset_horizontal( FoobarInsetContainer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_INSET_CONTAINER( self ), 0 ); + return self->inset_horizontal; +} + +// +// Get the inset applied to the top and bottom side of the widget. +// +gint foobar_inset_container_get_inset_vertical( FoobarInsetContainer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_INSET_CONTAINER( self ), 0 ); + return self->inset_vertical; +} + +// +// Update the container's child widget. +// +void foobar_inset_container_set_child( + FoobarInsetContainer* self, + GtkWidget* child ) +{ + g_return_if_fail( FOOBAR_IS_INSET_CONTAINER( self ) ); + g_return_if_fail( child == NULL || self->child == child || gtk_widget_get_parent( child ) == NULL ); + + if ( self->child != child ) + { + g_clear_pointer( &self->child, gtk_widget_unparent ); + + if ( child ) + { + self->child = child; + gtk_widget_set_parent( child, GTK_WIDGET( self ) ); + } + + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] ); + } +} + +// +// Update the inset applied to the left and right side of the widget. +// +void foobar_inset_container_set_inset_horizontal( + FoobarInsetContainer* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_INSET_CONTAINER( self ) ); + + if ( self->inset_horizontal != value ) + { + self->inset_horizontal = value; + gtk_widget_queue_resize( GTK_WIDGET( self ) ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_HORIZONTAL] ); + } +} + +// +// Update the inset applied to the top and bottom side of the widget. +// +void foobar_inset_container_set_inset_vertical( + FoobarInsetContainer* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_INSET_CONTAINER( self ) ); + + if ( self->inset_vertical != value ) + { + self->inset_vertical = value; + gtk_widget_queue_resize( GTK_WIDGET( self ) ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_VERTICAL] ); + } +}
\ No newline at end of file diff --git a/src/widgets/inset-container.h b/src/widgets/inset-container.h new file mode 100644 index 0000000..af0e6b1 --- /dev/null +++ b/src/widgets/inset-container.h @@ -0,0 +1,22 @@ +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_INSET_CONTAINER foobar_inset_container_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarInsetContainer, foobar_inset_container, FOOBAR, INSET_CONTAINER, GtkWidget ) + +GtkWidget* foobar_inset_container_new ( void ); +GtkWidget* foobar_inset_container_get_child ( FoobarInsetContainer* self ); +gint foobar_inset_container_get_inset_horizontal( FoobarInsetContainer* self ); +gint foobar_inset_container_get_inset_vertical ( FoobarInsetContainer* self ); +void foobar_inset_container_set_child ( FoobarInsetContainer* self, + GtkWidget* value ); +void foobar_inset_container_set_inset_horizontal( FoobarInsetContainer* self, + gint value ); +void foobar_inset_container_set_inset_vertical ( FoobarInsetContainer* self, + gint value ); + +G_END_DECLS
\ No newline at end of file diff --git a/src/widgets/limit-container.c b/src/widgets/limit-container.c new file mode 100644 index 0000000..d0686ef --- /dev/null +++ b/src/widgets/limit-container.c @@ -0,0 +1,341 @@ +#include "widgets/limit-container.h" + +// +// FoobarLimitContainer: +// +// A container which uses the natural size requested by the child as its minimum size, but only up to a certain point. +// +// This is useful because gtk_scrolled_window_set_max_content_height doesn't seem to be doing what I understand it to +// do. This way, we can set propagate_natural_height to TRUE and then limit the height through this container. +// + +struct _FoobarLimitContainer +{ + GtkWidget parent_instance; + GtkWidget* child; + gint max_width; + gint max_height; +}; + +enum +{ + PROP_CHILD = 1, + PROP_MAX_WIDTH, + PROP_MAX_HEIGHT, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_limit_container_class_init ( FoobarLimitContainerClass* klass ); +static void foobar_limit_container_init ( FoobarLimitContainer* self ); +static void foobar_limit_container_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_limit_container_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_limit_container_dispose ( GObject* object ); +static GtkSizeRequestMode foobar_limit_container_get_request_mode( GtkWidget* widget ); +static void foobar_limit_container_measure ( GtkWidget* widget, + GtkOrientation orientation, + int for_size, + int* minimum, + int* natural, + int* minimum_baseline, + int* natural_baseline ); +static void foobar_limit_container_size_allocate ( GtkWidget* widget, + int width, + int height, + int baseline ); + +G_DEFINE_FINAL_TYPE( FoobarLimitContainer, foobar_limit_container, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for limit containers. +// +void foobar_limit_container_class_init( FoobarLimitContainerClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + widget_klass->get_request_mode = foobar_limit_container_get_request_mode; + widget_klass->measure = foobar_limit_container_measure; + widget_klass->size_allocate = foobar_limit_container_size_allocate; + gtk_widget_class_set_css_name( widget_klass, "limit-container" ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_limit_container_get_property; + object_klass->set_property = foobar_limit_container_set_property; + object_klass->dispose = foobar_limit_container_dispose; + + props[PROP_CHILD] = g_param_spec_object( + "child", + "Child", + "Child widget for the panel item.", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE ); + props[PROP_MAX_WIDTH] = g_param_spec_int( + "max-width", + "Max Width", + "The maximum natural width supported as a minimum size.", + 0, + INT_MAX, + 0, + G_PARAM_READWRITE ); + props[PROP_MAX_HEIGHT] = g_param_spec_int( + "max-height", + "Max Height", + "The maximum natural height supported as a minimum size.", + 0, + INT_MAX, + 0, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for limit containers. +// +void foobar_limit_container_init( FoobarLimitContainer* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_limit_container_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarLimitContainer* self = (FoobarLimitContainer*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + g_value_set_object( value, foobar_limit_container_get_child( self ) ); + break; + case PROP_MAX_WIDTH: + g_value_set_int( value, foobar_limit_container_get_max_width( self ) ); + break; + case PROP_MAX_HEIGHT: + g_value_set_int( value, foobar_limit_container_get_max_height( 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_limit_container_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarLimitContainer* self = (FoobarLimitContainer*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + foobar_limit_container_set_child( self, g_value_get_object( value ) ); + break; + case PROP_MAX_WIDTH: + foobar_limit_container_set_max_width( self, g_value_get_int( value ) ); + break; + case PROP_MAX_HEIGHT: + foobar_limit_container_set_max_height( self, g_value_get_int( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for limit containers. +// +void foobar_limit_container_dispose( GObject* object ) +{ + FoobarLimitContainer* self = (FoobarLimitContainer*)object; + + g_clear_pointer( &self->child, gtk_widget_unparent ); + + G_OBJECT_CLASS( foobar_limit_container_parent_class )->dispose( object ); +} + +// +// Determine the size request mode used to measure the widget. +// +// This implementation just forwards the request mode from the child widget. +// +GtkSizeRequestMode foobar_limit_container_get_request_mode( GtkWidget* widget ) +{ + FoobarLimitContainer* self = (FoobarLimitContainer*)widget; + if ( !self->child ) { return GTK_SIZE_REQUEST_CONSTANT_SIZE; } + + return gtk_widget_get_request_mode( self->child ); +} + +// +// Compute the requested size of the widget. +// +void foobar_limit_container_measure( + GtkWidget* widget, + GtkOrientation orientation, + int for_size, + int* minimum, + int* natural, + int* minimum_baseline, + int* natural_baseline ) +{ + FoobarLimitContainer* self = (FoobarLimitContainer*)widget; + if ( !self->child ) + { + *minimum = 0; + *natural = 0; + *minimum_baseline = 0; + *natural_baseline = 0; + return; + } + + gtk_widget_measure( self->child, orientation, for_size, minimum, natural, minimum_baseline, natural_baseline ); + gint natural_minimum = *natural; + switch ( orientation ) + { + case GTK_ORIENTATION_HORIZONTAL: + natural_minimum = MIN( natural_minimum, self->max_width ); + break; + case GTK_ORIENTATION_VERTICAL: + natural_minimum = MIN( natural_minimum, self->max_height ); + break; + default: + g_warn_if_reached( ); + break; + } + *minimum = MAX( *minimum, natural_minimum ); +} + +// +// Arrange the child widget. +// +void foobar_limit_container_size_allocate( + GtkWidget* widget, + int width, + int height, + int baseline ) +{ + FoobarLimitContainer* self = (FoobarLimitContainer*)widget; + if ( !self->child ) { return; } + + gtk_widget_allocate( self->child, width, height, baseline, NULL ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new limit container instance. +// +GtkWidget* foobar_limit_container_new( void ) +{ + return g_object_new( FOOBAR_TYPE_LIMIT_CONTAINER, NULL ); +} + +// +// Get the container's child widget. +// +GtkWidget* foobar_limit_container_get_child( FoobarLimitContainer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ), NULL ); + return self->child; +} + +// +// Get the maximum natural width of the child to use as the minimum width for this container. +// +gint foobar_limit_container_get_max_width( FoobarLimitContainer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ), 0 ); + return self->max_width; +} + +// +// Get the maximum natural height of the child to use as the minimum height for this container. +// +gint foobar_limit_container_get_max_height( FoobarLimitContainer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ), 0 ); + return self->max_height; +} + +// +// Update the container's child widget. +// +void foobar_limit_container_set_child( + FoobarLimitContainer* self, + GtkWidget* child ) +{ + g_return_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ) ); + g_return_if_fail( child == NULL || self->child == child || gtk_widget_get_parent( child ) == NULL ); + + if ( self->child != child ) + { + g_clear_pointer( &self->child, gtk_widget_unparent ); + + if ( child ) + { + self->child = child; + gtk_widget_set_parent( child, GTK_WIDGET( self ) ); + } + + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] ); + } +} + +// +// Update the maximum natural width of the child to use as the minimum width for this container. +// +void foobar_limit_container_set_max_width( + FoobarLimitContainer* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ) ); + g_return_if_fail( value >= 0 ); + + if ( self->max_width != value ) + { + self->max_width = value; + gtk_widget_queue_resize( GTK_WIDGET( self ) ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_MAX_WIDTH] ); + } +} + +// +// Update the maximum natural height of the child to use as the minimum height for this container. +// +void foobar_limit_container_set_max_height( + FoobarLimitContainer* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ) ); + g_return_if_fail( value >= 0 ); + + if ( self->max_height != value ) + { + self->max_height = value; + gtk_widget_queue_resize( GTK_WIDGET( self ) ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_MAX_HEIGHT] ); + } +}
\ No newline at end of file diff --git a/src/widgets/limit-container.h b/src/widgets/limit-container.h new file mode 100644 index 0000000..c5e7e70 --- /dev/null +++ b/src/widgets/limit-container.h @@ -0,0 +1,22 @@ +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_LIMIT_CONTAINER foobar_limit_container_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarLimitContainer, foobar_limit_container, FOOBAR, LIMIT_CONTAINER, GtkWidget ) + +GtkWidget* foobar_limit_container_new ( void ); +GtkWidget* foobar_limit_container_get_child ( FoobarLimitContainer* self ); +gint foobar_limit_container_get_max_width ( FoobarLimitContainer* self ); +gint foobar_limit_container_get_max_height( FoobarLimitContainer* self ); +void foobar_limit_container_set_child ( FoobarLimitContainer* self, + GtkWidget* value ); +void foobar_limit_container_set_max_width ( FoobarLimitContainer* self, + gint value ); +void foobar_limit_container_set_max_height( FoobarLimitContainer* self, + gint value ); + +G_END_DECLS
\ No newline at end of file diff --git a/src/widgets/meson.build b/src/widgets/meson.build new file mode 100644 index 0000000..c6baefc --- /dev/null +++ b/src/widgets/meson.build @@ -0,0 +1,8 @@ +foobar_sources += files( + 'inset-container.c', + 'limit-container.c', + 'notification-widget.c', +) + +subdir('control-center') +subdir('panel')
\ No newline at end of file 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 diff --git a/src/widgets/notification-widget.h b/src/widgets/notification-widget.h new file mode 100644 index 0000000..3c15ee4 --- /dev/null +++ b/src/widgets/notification-widget.h @@ -0,0 +1,50 @@ +#pragma once + +#include <gtk/gtk.h> +#include "services/notification-service.h" + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION foobar_notification_close_action_get_type( ) +#define FOOBAR_TYPE_NOTIFICATION_WIDGET foobar_notification_widget_get_type( ) + +typedef enum +{ + FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE, + FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS, +} FoobarNotificationCloseAction; + +GType foobar_notification_close_action_get_type( void ); + +G_DECLARE_FINAL_TYPE( FoobarNotificationWidget, foobar_notification_widget, FOOBAR, NOTIFICATION_WIDGET, GtkWidget ) + +GtkWidget* foobar_notification_widget_new ( void ); +FoobarNotification* foobar_notification_widget_get_notification ( FoobarNotificationWidget* self ); +FoobarNotificationCloseAction foobar_notification_widget_get_close_action ( FoobarNotificationWidget* self ); +gchar const* foobar_notification_widget_get_time_format ( FoobarNotificationWidget* self ); +gint foobar_notification_widget_get_min_height ( FoobarNotificationWidget* self ); +gint foobar_notification_widget_get_close_button_inset( FoobarNotificationWidget* self ); +gint foobar_notification_widget_get_inset_start ( FoobarNotificationWidget* self ); +gint foobar_notification_widget_get_inset_end ( FoobarNotificationWidget* self ); +gint foobar_notification_widget_get_inset_top ( FoobarNotificationWidget* self ); +gint foobar_notification_widget_get_inset_bottom ( FoobarNotificationWidget* self ); +void foobar_notification_widget_set_notification ( FoobarNotificationWidget* self, + FoobarNotification* value ); +void foobar_notification_widget_set_close_action ( FoobarNotificationWidget* self, + FoobarNotificationCloseAction value ); +void foobar_notification_widget_set_time_format ( FoobarNotificationWidget* self, + gchar const* value ); +void foobar_notification_widget_set_min_height ( FoobarNotificationWidget* self, + gint value ); +void foobar_notification_widget_set_close_button_inset( FoobarNotificationWidget* self, + gint value ); +void foobar_notification_widget_set_inset_start ( FoobarNotificationWidget* self, + gint value ); +void foobar_notification_widget_set_inset_end ( FoobarNotificationWidget* self, + gint value ); +void foobar_notification_widget_set_inset_top ( FoobarNotificationWidget* self, + gint value ); +void foobar_notification_widget_set_inset_bottom ( FoobarNotificationWidget* self, + gint value ); + +G_END_DECLS
\ No newline at end of file diff --git a/src/widgets/panel/meson.build b/src/widgets/panel/meson.build new file mode 100644 index 0000000..abddd6d --- /dev/null +++ b/src/widgets/panel/meson.build @@ -0,0 +1,7 @@ +foobar_sources += files( + 'panel-item.c', + 'panel-item-icon.c', + 'panel-item-clock.c', + 'panel-item-workspaces.c', + 'panel-item-status.c', +)
\ No newline at end of file diff --git a/src/widgets/panel/panel-item-clock.c b/src/widgets/panel/panel-item-clock.c new file mode 100644 index 0000000..1efd7fa --- /dev/null +++ b/src/widgets/panel/panel-item-clock.c @@ -0,0 +1,152 @@ +#include "widgets/panel/panel-item-clock.h" + +// +// FoobarPanelItemClock: +// +// A panel item displaying the current time in the format configured by the user. +// + +struct _FoobarPanelItemClock +{ + FoobarPanelItem parent_instance; + gchar* format; + FoobarPanelItemAction action; + GtkWidget* label; + GtkWidget* button; + FoobarClockService* clock_service; +}; + +static void foobar_panel_item_clock_class_init ( FoobarPanelItemClockClass* klass ); +static void foobar_panel_item_clock_init ( FoobarPanelItemClock* self ); +static void foobar_panel_item_clock_finalize ( GObject* object ); +static void foobar_panel_item_clock_handle_clicked( GtkButton* button, + gpointer userdata ); +static gchar* foobar_panel_item_clock_compute_label ( GtkExpression* expression, + GDateTime* time, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarPanelItemClock, foobar_panel_item_clock, FOOBAR_TYPE_PANEL_ITEM ) + +// --------------------------------------------------------------------------------------------------------------------- +// Panel Item Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for clock panel items. +// +void foobar_panel_item_clock_class_init( FoobarPanelItemClockClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->finalize = foobar_panel_item_clock_finalize; +} + +// +// Instance initialization for clock panel items. +// +void foobar_panel_item_clock_init( FoobarPanelItemClock* self ) +{ + self->label = gtk_label_new( NULL ); + gtk_label_set_justify( GTK_LABEL( self->label ), GTK_JUSTIFY_CENTER ); + + self->button = gtk_button_new( ); + gtk_button_set_has_frame( GTK_BUTTON( self->button ), FALSE ); + gtk_button_set_child( GTK_BUTTON( self->button ), self->label ); + gtk_widget_set_halign( self->button, GTK_ALIGN_CENTER ); + gtk_widget_set_valign( self->button, GTK_ALIGN_CENTER ); + g_signal_connect( + self->button, + "clicked", + G_CALLBACK( foobar_panel_item_clock_handle_clicked ), + self ); + + gtk_widget_add_css_class( GTK_WIDGET( self ), "clock" ); + foobar_panel_item_set_child( FOOBAR_PANEL_ITEM( self ), self->button ); +} + +// +// Instance cleanup for clock panel items. +// +void foobar_panel_item_clock_finalize( GObject* object ) +{ + FoobarPanelItemClock* self = (FoobarPanelItemClock*)object; + + g_clear_pointer( &self->format, g_free ); + g_clear_object( &self->clock_service ); + + G_OBJECT_CLASS( foobar_panel_item_clock_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new clock panel item with the given configuration. +// +FoobarPanelItem* foobar_panel_item_clock_new( + FoobarPanelItemConfiguration const* config, + FoobarClockService* clock_service ) +{ + g_return_val_if_fail( config != NULL, NULL ); + g_return_val_if_fail( foobar_panel_item_configuration_get_kind( config ) == FOOBAR_PANEL_ITEM_KIND_CLOCK, NULL ); + g_return_val_if_fail( FOOBAR_IS_CLOCK_SERVICE( clock_service ), NULL ); + + FoobarPanelItemClock* self = g_object_new( FOOBAR_TYPE_PANEL_ITEM_CLOCK, NULL ); + self->format = g_strdup( foobar_panel_item_clock_configuration_get_format( config ) ); + self->action = foobar_panel_item_configuration_get_action( config ); + self->clock_service = g_object_ref( clock_service ); + + gtk_widget_set_sensitive( self->button, self->action != FOOBAR_PANEL_ITEM_ACTION_NONE ); + + { + GtkExpression* service_expr = gtk_constant_expression_new( FOOBAR_TYPE_CLOCK_SERVICE, self->clock_service ); + GtkExpression* time_expr = gtk_property_expression_new( FOOBAR_TYPE_CLOCK_SERVICE, service_expr, "time" ); + GtkExpression* label_params[] = { time_expr }; + GtkExpression* label_expr = gtk_cclosure_expression_new( + G_TYPE_STRING, + NULL, + G_N_ELEMENTS( label_params ), + label_params, + G_CALLBACK( foobar_panel_item_clock_compute_label ), + self, + NULL ); + gtk_expression_bind( label_expr, self->label, "label", NULL ); + } + + return FOOBAR_PANEL_ITEM( self ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called when the button was clicked. +// +void foobar_panel_item_clock_handle_clicked( + GtkButton* button, + gpointer userdata ) +{ + (void)button; + FoobarPanelItemClock* self = (FoobarPanelItemClock*)userdata; + + foobar_panel_item_invoke_action( FOOBAR_PANEL_ITEM( self ), self->action ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Value Converters +// --------------------------------------------------------------------------------------------------------------------- + +// +// Derive the label from a timestamp, depending on the configured time format. +// +gchar* foobar_panel_item_clock_compute_label( + GtkExpression* expression, + GDateTime* time, + gpointer userdata ) +{ + (void)expression; + FoobarPanelItemClock* self = (FoobarPanelItemClock*)userdata; + + return g_date_time_format( time, self->format ); +}
\ No newline at end of file diff --git a/src/widgets/panel/panel-item-clock.h b/src/widgets/panel/panel-item-clock.h new file mode 100644 index 0000000..244da2f --- /dev/null +++ b/src/widgets/panel/panel-item-clock.h @@ -0,0 +1,15 @@ +#pragma once + +#include "widgets/panel/panel-item.h" +#include "services/clock-service.h" + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_PANEL_ITEM_CLOCK foobar_panel_item_clock_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarPanelItemClock, foobar_panel_item_clock, FOOBAR, PANEL_ITEM_CLOCK, FoobarPanelItem ) + +FoobarPanelItem* foobar_panel_item_clock_new( FoobarPanelItemConfiguration const* config, + FoobarClockService* clock_service ); + +G_END_DECLS
\ No newline at end of file diff --git a/src/widgets/panel/panel-item-icon.c b/src/widgets/panel/panel-item-icon.c new file mode 100644 index 0000000..327f027 --- /dev/null +++ b/src/widgets/panel/panel-item-icon.c @@ -0,0 +1,92 @@ +#include "widgets/panel/panel-item-icon.h" + +// +// FoobarPanelItemIcon: +// +// Simple item implementation displaying an icon. +// + +struct _FoobarPanelItemIcon +{ + FoobarPanelItem parent_instance; + GtkWidget* button; + FoobarPanelItemAction action; +}; + +static void foobar_panel_item_icon_class_init ( FoobarPanelItemIconClass* klass ); +static void foobar_panel_item_icon_init ( FoobarPanelItemIcon* self ); +static void foobar_panel_item_icon_handle_clicked( GtkButton* button, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarPanelItemIcon, foobar_panel_item_icon, FOOBAR_TYPE_PANEL_ITEM ) + +// --------------------------------------------------------------------------------------------------------------------- +// Panel Item Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for icon panel items. +// +void foobar_panel_item_icon_class_init( FoobarPanelItemIconClass* klass ) +{ + (void)klass; +} + +// +// Instance initialization for icon panel items. +// +void foobar_panel_item_icon_init( FoobarPanelItemIcon* self ) +{ + self->button = gtk_button_new( ); + gtk_button_set_has_frame( GTK_BUTTON( self->button ), FALSE ); + gtk_widget_set_halign( self->button, GTK_ALIGN_CENTER ); + gtk_widget_set_valign( self->button, GTK_ALIGN_CENTER ); + g_signal_connect( + self->button, + "clicked", + G_CALLBACK( foobar_panel_item_icon_handle_clicked ), + self ); + + gtk_widget_add_css_class( GTK_WIDGET( self ), "icon" ); + foobar_panel_item_set_child( FOOBAR_PANEL_ITEM( self ), self->button ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new icon panel item with the given configuration. +// +FoobarPanelItem* foobar_panel_item_icon_new( FoobarPanelItemConfiguration const* config ) +{ + g_return_val_if_fail( config != NULL, NULL ); + g_return_val_if_fail( foobar_panel_item_configuration_get_kind( config ) == FOOBAR_PANEL_ITEM_KIND_ICON, NULL ); + + FoobarPanelItemIcon* self = g_object_new( FOOBAR_TYPE_PANEL_ITEM_ICON, NULL ); + self->action = foobar_panel_item_configuration_get_action( config ); + + gtk_widget_set_sensitive( self->button, self->action != FOOBAR_PANEL_ITEM_ACTION_NONE ); + gtk_button_set_icon_name( + GTK_BUTTON( self->button ), + foobar_panel_item_icon_configuration_get_icon_name( config ) ); + + return FOOBAR_PANEL_ITEM( self ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called when the button was clicked. +// +void foobar_panel_item_icon_handle_clicked( + GtkButton* button, + gpointer userdata ) +{ + (void)button; + FoobarPanelItemIcon* self = (FoobarPanelItemIcon*)userdata; + + foobar_panel_item_invoke_action( FOOBAR_PANEL_ITEM( self ), self->action ); +}
\ No newline at end of file diff --git a/src/widgets/panel/panel-item-icon.h b/src/widgets/panel/panel-item-icon.h new file mode 100644 index 0000000..77b1144 --- /dev/null +++ b/src/widgets/panel/panel-item-icon.h @@ -0,0 +1,13 @@ +#pragma once + +#include "widgets/panel/panel-item.h" + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_PANEL_ITEM_ICON foobar_panel_item_icon_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarPanelItemIcon, foobar_panel_item_icon, FOOBAR, PANEL_ITEM_ICON, FoobarPanelItem ) + +FoobarPanelItem* foobar_panel_item_icon_new( FoobarPanelItemConfiguration const* config ); + +G_END_DECLS
\ No newline at end of file diff --git a/src/widgets/panel/panel-item-status.c b/src/widgets/panel/panel-item-status.c new file mode 100644 index 0000000..4deacb4 --- /dev/null +++ b/src/widgets/panel/panel-item-status.c @@ -0,0 +1,955 @@ +#include "widgets/panel/panel-item-status.h" + +// +// FoobarPanelItemStatus: +// +// A panel item showing status indicators for various things such as network state, audio volume, screen brightness, +// battery level, etc. +// + +struct _FoobarPanelItemStatus +{ + FoobarPanelItem parent_instance; + FoobarPanelItemAction action; + GtkWidget* button; + GtkWidget* layout; + FoobarBatteryService* battery_service; + FoobarBrightnessService* brightness_service; + FoobarAudioService* audio_service; + FoobarNetworkService* network_service; + FoobarBluetoothService* bluetooth_service; + FoobarNotificationService* notification_service; + GtkWidget* network_icon; + GtkWidget* network_label; + GtkExpression* network_icon_expr; + FoobarNetwork* network; + gulong network_handler_id; + gulong network_strength_handler_id; +}; + +enum +{ + PROP_ORIENTATION = 1, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_panel_item_status_class_init ( FoobarPanelItemStatusClass* klass ); +static void foobar_panel_item_status_init ( FoobarPanelItemStatus* self ); +static void foobar_panel_item_status_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_panel_item_status_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_panel_item_status_finalize ( GObject* object ); +static void foobar_panel_item_status_handle_network_change ( FoobarNetworkAdapterWifi* adapter, + GParamSpec* pspec, + gpointer userdata ); +static void foobar_panel_item_status_handle_network_strength_change( GObject* object, + GParamSpec* pspec, + gpointer userdata ); +static void foobar_panel_item_status_handle_clicked ( GtkButton* button, + gpointer userdata ); +static gboolean foobar_panel_item_status_handle_brightness_scroll ( GtkEventControllerScroll* controller, + gdouble dx, + gdouble dy, + gpointer userdata ); +static gboolean foobar_panel_item_status_handle_audio_scroll ( GtkEventControllerScroll* controller, + gdouble dx, + gdouble dy, + gpointer userdata ); +static gchar* foobar_panel_item_status_compute_network_icon ( GtkExpression* expression, + FoobarNetworkAdapter* adapter, + FoobarNetworkAdapterState state, + gpointer userdata ); +static gchar* foobar_panel_item_status_compute_bluetooth_icon ( GtkExpression* expression, + gboolean is_enabled, + guint connected_device_count, + gpointer userdata ); +static gchar* foobar_panel_item_status_compute_battery_icon ( GtkExpression* expression, + FoobarBatteryState const* state, + gpointer userdata ); +static gchar* foobar_panel_item_status_compute_battery_label ( GtkExpression* expression, + FoobarBatteryState const* state, + gpointer userdata ); +static gchar* foobar_panel_item_status_compute_brightness_icon ( GtkExpression* expression, + gint percentage, + gpointer userdata ); +static gchar* foobar_panel_item_status_compute_brightness_label ( GtkExpression* expression, + gint percentage, + gpointer userdata ); +static gchar* foobar_panel_item_status_compute_audio_icon ( GtkExpression* expression, + gboolean is_available, + gint volume, + gpointer userdata ); +static gchar* foobar_panel_item_status_compute_audio_label ( GtkExpression* expression, + gboolean is_available, + gint volume, + gpointer userdata ); +static gchar* foobar_panel_item_status_compute_notifications_icon ( GtkExpression* expression, + guint count, + gpointer userdata ); +static GtkWidget* foobar_panel_item_status_create_item ( FoobarPanelItemStatus* self, + FoobarStatusItem item, + gboolean show_labels, + gboolean enable_scrolling ); +static void foobar_panel_item_status_reevaluate_network ( FoobarPanelItemStatus* self ); + +G_DEFINE_FINAL_TYPE_WITH_CODE( + FoobarPanelItemStatus, + foobar_panel_item_status, + FOOBAR_TYPE_PANEL_ITEM, + G_IMPLEMENT_INTERFACE( GTK_TYPE_ORIENTABLE, NULL ) ) + +// --------------------------------------------------------------------------------------------------------------------- +// Panel Item Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for status panel items. +// +void foobar_panel_item_status_class_init( FoobarPanelItemStatusClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_panel_item_status_get_property; + object_klass->set_property = foobar_panel_item_status_set_property; + object_klass->finalize = foobar_panel_item_status_finalize; + + gpointer orientable_iface = g_type_default_interface_peek( GTK_TYPE_ORIENTABLE ); + props[PROP_ORIENTATION] = g_param_spec_override( + "orientation", + g_object_interface_find_property( orientable_iface, "orientation" ) ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for status panel items. +// +void foobar_panel_item_status_init( FoobarPanelItemStatus* self ) +{ + self->layout = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); + + self->button = gtk_button_new( ); + gtk_button_set_has_frame( GTK_BUTTON( self->button ), FALSE ); + gtk_button_set_child( GTK_BUTTON( self->button ), self->layout ); + gtk_widget_set_halign( self->button, GTK_ALIGN_CENTER ); + gtk_widget_set_valign( self->button, GTK_ALIGN_CENTER ); + g_signal_connect( + self->button, + "clicked", + G_CALLBACK( foobar_panel_item_status_handle_clicked ), + self ); + + gtk_widget_add_css_class( GTK_WIDGET( self ), "status" ); + foobar_panel_item_set_child( FOOBAR_PANEL_ITEM( self ), self->button ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_panel_item_status_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)object; + + switch ( prop_id ) + { + case PROP_ORIENTATION: + g_value_set_enum( value, gtk_orientable_get_orientation( GTK_ORIENTABLE( self->layout ) ) ); + 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_panel_item_status_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)object; + + switch ( prop_id ) + { + case PROP_ORIENTATION: + gtk_orientable_set_orientation( GTK_ORIENTABLE( self->layout ), g_value_get_enum( value ) ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ORIENTATION] ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for status panel items. +// +void foobar_panel_item_status_finalize( GObject* object ) +{ + FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)object; + + FoobarNetworkAdapterWifi* adapter = foobar_network_service_get_wifi( self->network_service ); + g_clear_signal_handler( &self->network_handler_id, adapter ); + g_clear_signal_handler( &self->network_strength_handler_id, self->network ); + g_clear_object( &self->network ); + g_clear_object( &self->battery_service ); + g_clear_object( &self->brightness_service ); + g_clear_object( &self->audio_service ); + g_clear_object( &self->network_service ); + g_clear_object( &self->bluetooth_service ); + g_clear_object( &self->notification_service ); + g_clear_pointer( &self->network_icon_expr, gtk_expression_unref ); + + G_OBJECT_CLASS( foobar_panel_item_status_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new status panel item with the given configuration. +// +FoobarPanelItem* foobar_panel_item_status_new( + FoobarPanelItemConfiguration const* config, + FoobarBatteryService* battery_service, + FoobarBrightnessService* brightness_service, + FoobarAudioService* audio_service, + FoobarNetworkService* network_service, + FoobarBluetoothService* bluetooth_service, + FoobarNotificationService* notification_service ) +{ + g_return_val_if_fail( config != NULL, NULL ); + g_return_val_if_fail( foobar_panel_item_configuration_get_kind( config ) == FOOBAR_PANEL_ITEM_KIND_STATUS, NULL ); + g_return_val_if_fail( FOOBAR_IS_BATTERY_SERVICE( battery_service ), NULL ); + g_return_val_if_fail( FOOBAR_IS_BRIGHTNESS_SERVICE( brightness_service ), NULL ); + g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( audio_service ), NULL ); + g_return_val_if_fail( FOOBAR_IS_NETWORK_SERVICE( network_service ), NULL ); + g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( bluetooth_service ), NULL ); + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( notification_service ), NULL ); + + FoobarPanelItemStatus* self = g_object_new( FOOBAR_TYPE_PANEL_ITEM_STATUS, NULL ); + self->action = foobar_panel_item_configuration_get_action( config ); + self->battery_service = g_object_ref( battery_service ); + self->brightness_service = g_object_ref( brightness_service ); + self->audio_service = g_object_ref( audio_service ); + self->network_service = g_object_ref( network_service ); + self->bluetooth_service = g_object_ref( bluetooth_service ); + self->notification_service = g_object_ref( notification_service ); + + gtk_widget_set_sensitive( self->button, self->action != FOOBAR_PANEL_ITEM_ACTION_NONE ); + + gtk_box_set_spacing( GTK_BOX( self->layout ), foobar_panel_item_status_configuration_get_spacing( config ) ); + gboolean show_labels = foobar_panel_item_status_configuration_get_show_labels( config ); + gboolean enable_scrolling = foobar_panel_item_status_configuration_get_enable_scrolling( config ); + gsize items_count; + FoobarStatusItem const* items = foobar_panel_item_status_configuration_get_items( config, &items_count ); + for ( gsize i = 0; i < items_count; ++i ) + { + GtkWidget* widget = foobar_panel_item_status_create_item( self, items[i], show_labels, enable_scrolling ); + if ( widget ) + { + gtk_widget_set_halign( widget, GTK_ALIGN_CENTER ); + gtk_box_append( GTK_BOX( self->layout ), widget ); + } + } + + return FOOBAR_PANEL_ITEM( self ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called when the wi-fi adapter's active network changes. +// +// This is used to then subscribe to changes to the active network's strength. Unfortunately, we can't just use +// GtkExpressions here because the active network may be NULL in which case the strength expression would fail. If one +// expression fails to evaluate, then the closure expression used to derive the network icon/label also does not get +// updated. +// +// In addition, this method also updates the network label to show the active network's name or become invisible. +// +void foobar_panel_item_status_handle_network_change( + FoobarNetworkAdapterWifi* adapter, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)pspec; + FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)userdata; + + FoobarNetwork* network = foobar_network_adapter_wifi_get_active( adapter ); + if ( self->network != network ) + { + g_clear_signal_handler( &self->network_strength_handler_id, self->network ); + g_clear_object( &self->network ); + + if ( network ) + { + self->network = g_object_ref( network ); + self->network_strength_handler_id = g_signal_connect( + self->network, + "notify::strength", + G_CALLBACK( foobar_panel_item_status_handle_network_strength_change ), + self ); + } + + if ( self->network_label ) + { + gtk_label_set_label( + GTK_LABEL( self->network_label ), self->network ? foobar_network_get_name( self->network ) : NULL ); + gtk_widget_set_visible( GTK_WIDGET( self->network_label ), self->network != NULL ); + } + + foobar_panel_item_status_reevaluate_network( self ); + } +} + +// +// Called when the wi-fi adapter's active network strength changes. +// +// This just forces re-evaluation of the network icon expression for reasons mentioned in +// foobar_panel_item_status_handle_network_change. +// +void foobar_panel_item_status_handle_network_strength_change( + GObject* object, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)object; + (void)pspec; + FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)userdata; + foobar_panel_item_status_reevaluate_network( self ); +} + +// +// Called when the button was clicked. +// +void foobar_panel_item_status_handle_clicked( + GtkButton* button, + gpointer userdata ) +{ + (void)button; + FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)userdata; + + foobar_panel_item_invoke_action( FOOBAR_PANEL_ITEM( self ), self->action ); +} + +// +// Called when the user has scrolled over the brightness item, used to adjust the brightness level. +// +gboolean foobar_panel_item_status_handle_brightness_scroll( + GtkEventControllerScroll* controller, + gdouble dx, + gdouble dy, + gpointer userdata ) +{ + (void)controller; + (void)dx; + FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)userdata; + + gint new_brightness = foobar_brightness_service_get_percentage( self->brightness_service ) + (gint)dy; + foobar_brightness_service_set_percentage( self->brightness_service, new_brightness ); + + return TRUE; +} + +// +// Called when the user has scrolled over the audio item, used to adjust the volume. +// +gboolean foobar_panel_item_status_handle_audio_scroll( + GtkEventControllerScroll* controller, + gdouble dx, + gdouble dy, + gpointer userdata ) +{ + (void)controller; + (void)dx; + FoobarPanelItemStatus* self = (FoobarPanelItemStatus*)userdata; + + FoobarAudioDevice* device = foobar_audio_service_get_default_output( self->audio_service ); + gint new_volume = foobar_audio_device_get_volume( device ) + (gint)dy; + foobar_audio_device_set_volume( device, new_volume ); + + return TRUE; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Value Converters +// --------------------------------------------------------------------------------------------------------------------- + +// +// Derive the network icon from the active network adapter and its state. +// +// In addition, this also uses the wi-fi adapter's active network strength which is monitored separately (see +// foobar_panel_item_status_handle_network_change). +// +gchar* foobar_panel_item_status_compute_network_icon( + GtkExpression* expression, + FoobarNetworkAdapter* adapter, + FoobarNetworkAdapterState state, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + if ( FOOBAR_IS_NETWORK_ADAPTER_WIFI( adapter ) ) + { + if ( state == FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED ) { return g_strdup( "fluent-wifi-off-symbolic" ); } + if ( state != FOOBAR_NETWORK_ADAPTER_STATE_CONNECTED ) { return g_strdup( "fluent-wifi-warning-symbolic" ); } + + FoobarNetwork* network = foobar_network_adapter_wifi_get_active( FOOBAR_NETWORK_ADAPTER_WIFI( adapter ) ); + gint strength = network ? foobar_network_get_strength( network ) : 0; + if ( strength <= 25 ) { return g_strdup( "fluent-wifi-4-symbolic" ); } + if ( strength <= 50 ) { return g_strdup( "fluent-wifi-3-symbolic" ); } + if ( strength <= 75 ) { return g_strdup( "fluent-wifi-2-symbolic" ); } + return g_strdup( "fluent-wifi-1-symbolic" ); + } + else + { + return g_strdup( "fluent-virtual-network-symbolic" ); + } +} + +// +// Derive the bluetooth icon from the bluetooth adapter's state and the number of currently connected devices. +// +gchar* foobar_panel_item_status_compute_bluetooth_icon( + GtkExpression* expression, + gboolean is_enabled, + guint connected_device_count, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + if (is_enabled) + { + return ( connected_device_count > 0 ) + ? g_strdup( "fluent-bluetooth-connected-symbolic" ) + : g_strdup( "fluent-bluetooth-symbolic" ); + } + else + { + return g_strdup( "fluent-bluetooth-off-symbolic" ); + } +} + +// +// Derive the battery icon from the current state structure. +// +gchar* foobar_panel_item_status_compute_battery_icon( + GtkExpression* expression, + FoobarBatteryState const* state, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + if ( state ) + { + gint percentage = foobar_battery_state_get_percentage( state ); + if ( foobar_battery_state_is_charging( state ) ) { return g_strdup( "fluent-battery-charge-symbolic" ); } + if ( percentage <= 5 ) { return g_strdup( "fluent-battery-0-symbolic" ); } + if ( percentage <= 15 ) { return g_strdup( "fluent-battery-1-symbolic" ); } + if ( percentage <= 25 ) { return g_strdup( "fluent-battery-2-symbolic" ); } + if ( percentage <= 35 ) { return g_strdup( "fluent-battery-3-symbolic" ); } + if ( percentage <= 45 ) { return g_strdup( "fluent-battery-4-symbolic" ); } + if ( percentage <= 55 ) { return g_strdup( "fluent-battery-5-symbolic" ); } + if ( percentage <= 65 ) { return g_strdup( "fluent-battery-6-symbolic" ); } + if ( percentage <= 75 ) { return g_strdup( "fluent-battery-7-symbolic" ); } + if ( percentage <= 85 ) { return g_strdup( "fluent-battery-8-symbolic" ); } + if ( percentage <= 95 ) { return g_strdup( "fluent-battery-9-symbolic" ); } + return g_strdup( "fluent-battery-10-symbolic" ); + } + else + { + return g_strdup( "fluent-plug-connected-symbolic" ); + } +} + +// +// Derive the battery label from the current state structure, showing the percentage if available. +// +gchar* foobar_panel_item_status_compute_battery_label( + GtkExpression* expression, + FoobarBatteryState const* state, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + if ( state ) + { + return g_strdup_printf( "%d%%", foobar_battery_state_get_percentage( state ) ); + } + else + { + return g_strdup( "n/a" ); + } +} + +// +// Derive the brightness icon from the current percentage value. +// +gchar* foobar_panel_item_status_compute_brightness_icon( + GtkExpression* expression, + gint percentage, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + if ( percentage <= 50 ) + { + return g_strdup( "fluent-brightness-low-symbolic" ); + } + else + { + return g_strdup( "fluent-brightness-high-symbolic" ); + } +} + +// +// Derive the brightness label from the current percentage value. +// +gchar* foobar_panel_item_status_compute_brightness_label( + GtkExpression* expression, + gint percentage, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + return g_strdup_printf( "%d%%", percentage ); +} + +// +// Derive the audio icon from the current state and volume of the default output device. +// +gchar* foobar_panel_item_status_compute_audio_icon( + GtkExpression* expression, + gboolean is_available, + gint volume, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + if ( is_available ) + { + if ( volume == 0 ) { return g_strdup( "fluent-speaker-mute-symbolic" ); } + if ( volume <= 33 ) { return g_strdup( "fluent-speaker-0-symbolic" ); } + if ( volume <= 67 ) { return g_strdup( "fluent-speaker-1-symbolic" ); } + return g_strdup( "fluent-speaker-2-symbolic" ); + } + else + { + return g_strdup( "fluent-speaker-off-symbolic" ); + } +} + +// +// Derive the audio label from the current state and volume of the default output device. +// +gchar* foobar_panel_item_status_compute_audio_label( + GtkExpression* expression, + gboolean is_available, + gint volume, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + if ( is_available ) + { + return g_strdup_printf( "%d%%", volume ); + } + else + { + return g_strdup( "n/a" ); + } +} + +// +// Derive the notification icon from the current number of notifications. +// +gchar* foobar_panel_item_status_compute_notifications_icon( + GtkExpression* expression, + guint count, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + switch ( count ) + { + case 0: + return g_strdup( "fluent-checkmark-circle-symbolic" ); + case 1: + return g_strdup( "fluent-number-circle-1-symbolic" ); + case 2: + return g_strdup( "fluent-number-circle-2-symbolic" ); + case 3: + return g_strdup( "fluent-number-circle-3-symbolic" ); + case 4: + return g_strdup( "fluent-number-circle-4-symbolic" ); + case 5: + return g_strdup( "fluent-number-circle-5-symbolic" ); + case 6: + return g_strdup( "fluent-number-circle-6-symbolic" ); + case 7: + return g_strdup( "fluent-number-circle-7-symbolic" ); + case 8: + return g_strdup( "fluent-number-circle-8-symbolic" ); + case 9: + return g_strdup( "fluent-number-circle-9-symbolic" ); + default: + return g_strdup( "fluent-more-circle-symbolic" ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new icon displaying the specified status item, optionally adding a label to it (if supported). +// +GtkWidget* foobar_panel_item_status_create_item( + FoobarPanelItemStatus* self, + FoobarStatusItem item, + gboolean show_labels, + gboolean enable_scrolling ) +{ + switch ( item ) + { + case FOOBAR_STATUS_ITEM_NETWORK: + { + GtkWidget* box = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); + gtk_widget_set_valign( box, GTK_ALIGN_FILL ); + + GtkExpression* service_expr = gtk_constant_expression_new( + FOOBAR_TYPE_NETWORK_SERVICE, + self->network_service ); + GtkExpression* active_expr = gtk_property_expression_new( + FOOBAR_TYPE_NETWORK_SERVICE, + service_expr, + "active" ); + GtkExpression* state_expr = gtk_property_expression_new( + FOOBAR_TYPE_NETWORK_ADAPTER, + gtk_expression_ref( active_expr ), + "state" ); + GtkExpression* icon_params[] = { active_expr, state_expr }; + self->network_icon_expr = gtk_cclosure_expression_new( + G_TYPE_STRING, + NULL, + G_N_ELEMENTS( icon_params ), + icon_params, + G_CALLBACK( foobar_panel_item_status_compute_network_icon ), + self, + NULL ); + self->network_icon = gtk_image_new( ); + gtk_widget_set_valign( self->network_icon, GTK_ALIGN_CENTER ); + gtk_expression_bind( gtk_expression_ref( self->network_icon_expr ), self->network_icon, "icon-name", NULL ); + gtk_box_append( GTK_BOX( box ), self->network_icon ); + + if ( show_labels ) + { + self->network_label = gtk_label_new( NULL ); + gtk_widget_set_valign( self->network_label, GTK_ALIGN_CENTER ); + gtk_label_set_max_width_chars( GTK_LABEL( self->network_label ), 16 ); + gtk_label_set_ellipsize( GTK_LABEL( self->network_label ), PANGO_ELLIPSIZE_END ); + gtk_box_append( GTK_BOX( box ), self->network_label ); + } + + // Manually subscribe to network (/strength) because evaluation might fail. + + FoobarNetworkAdapterWifi* adapter = foobar_network_service_get_wifi( self->network_service ); + if ( adapter ) + { + self->network_handler_id = g_signal_connect( + adapter, + "notify::active", + G_CALLBACK( foobar_panel_item_status_handle_network_change ), + self ); + if ( foobar_network_adapter_wifi_get_active( adapter ) ) + { + self->network = g_object_ref( foobar_network_adapter_wifi_get_active( adapter ) ); + self->network_strength_handler_id = g_signal_connect( + self->network, + "notify::strength", + G_CALLBACK( foobar_panel_item_status_handle_network_strength_change ), + self ); + } + if ( self->network_label ) + { + gtk_label_set_label( + GTK_LABEL( self->network_label ), + self->network ? foobar_network_get_name( self->network ) : NULL ); + gtk_widget_set_visible( GTK_WIDGET( self->network_label ), self->network != NULL ); + } + } + + return box; + } + case FOOBAR_STATUS_ITEM_BLUETOOTH: + { + GtkWidget* icon = gtk_image_new( ); + gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); + + GtkExpression* service_expr = gtk_constant_expression_new( + FOOBAR_TYPE_BLUETOOTH_SERVICE, + self->bluetooth_service ); + GtkExpression* enabled_expr = gtk_property_expression_new( + FOOBAR_TYPE_BLUETOOTH_SERVICE, + gtk_expression_ref( service_expr ), + "is-enabled" ); + GtkExpression* connected_devices_expr = gtk_property_expression_new( + FOOBAR_TYPE_BLUETOOTH_SERVICE, + service_expr, + "connected-devices" ); + GtkExpression* connected_device_count_expr = gtk_property_expression_new( + GTK_TYPE_FILTER_LIST_MODEL, + connected_devices_expr, + "n-items" ); + GtkExpression* icon_params[] = { enabled_expr, connected_device_count_expr }; + GtkExpression* icon_expr = gtk_cclosure_expression_new( + G_TYPE_STRING, + NULL, + G_N_ELEMENTS( icon_params ), + icon_params, + G_CALLBACK( foobar_panel_item_status_compute_bluetooth_icon ), + NULL, + NULL ); + gtk_expression_bind( icon_expr, icon, "icon-name", NULL ); + + return icon; + } + case FOOBAR_STATUS_ITEM_BATTERY: + { + GtkWidget* box = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); + gtk_widget_set_valign( box, GTK_ALIGN_FILL ); + + GtkExpression* service_expr = gtk_constant_expression_new( + FOOBAR_TYPE_BATTERY_SERVICE, + self->battery_service ); + GtkExpression* state_expr = gtk_property_expression_new( + FOOBAR_TYPE_BATTERY_SERVICE, + service_expr, + "state" ); + + GtkExpression* icon_params[] = { state_expr }; + GtkExpression* icon_expr = gtk_cclosure_expression_new( + G_TYPE_STRING, + NULL, + G_N_ELEMENTS( icon_params ), + icon_params, + G_CALLBACK( foobar_panel_item_status_compute_battery_icon ), + NULL, + NULL ); + GtkWidget* icon = gtk_image_new( ); + gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); + gtk_expression_bind( icon_expr, icon, "icon-name", NULL ); + gtk_box_append( GTK_BOX( box ), icon ); + + if ( show_labels ) + { + GtkExpression* label_params[] = { gtk_expression_ref( state_expr ) }; + GtkExpression* label_expr = gtk_cclosure_expression_new( + G_TYPE_STRING, + NULL, + G_N_ELEMENTS( label_params ), + label_params, + G_CALLBACK( foobar_panel_item_status_compute_battery_label ), + NULL, + NULL ); + GtkWidget* label = gtk_label_new( NULL ); + gtk_widget_set_valign( label, GTK_ALIGN_CENTER ); + gtk_expression_bind( label_expr, label, "label", NULL ); + gtk_box_append( GTK_BOX( box ), label ); + } + + return box; + } + case FOOBAR_STATUS_ITEM_BRIGHTNESS: + { + GtkWidget* box = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); + gtk_widget_set_valign( box, GTK_ALIGN_FILL ); + + if ( enable_scrolling ) + { + GtkEventController* scroll_controller = gtk_event_controller_scroll_new( + GTK_EVENT_CONTROLLER_SCROLL_VERTICAL | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE ); + g_signal_connect( + scroll_controller, + "scroll", + G_CALLBACK( foobar_panel_item_status_handle_brightness_scroll ), + self ); + gtk_widget_add_controller( box, scroll_controller ); + } + + GtkExpression* service_expr = gtk_constant_expression_new( + FOOBAR_TYPE_BRIGHTNESS_SERVICE, + self->brightness_service ); + GtkExpression* percentage_expr = gtk_property_expression_new( + FOOBAR_TYPE_BRIGHTNESS_SERVICE, + service_expr, + "percentage" ); + + GtkExpression* icon_params[] = { percentage_expr }; + GtkExpression* icon_expr = gtk_cclosure_expression_new( + G_TYPE_STRING, + NULL, + G_N_ELEMENTS( icon_params ), + icon_params, + G_CALLBACK( foobar_panel_item_status_compute_brightness_icon ), + NULL, + NULL ); + GtkWidget* icon = gtk_image_new( ); + gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); + gtk_expression_bind( icon_expr, icon, "icon-name", NULL ); + gtk_box_append( GTK_BOX( box ), icon ); + + if ( show_labels ) + { + GtkExpression* label_params[] = { gtk_expression_ref( percentage_expr ) }; + GtkExpression* label_expr = gtk_cclosure_expression_new( + G_TYPE_STRING, + NULL, + G_N_ELEMENTS( label_params ), + label_params, + G_CALLBACK( foobar_panel_item_status_compute_brightness_label ), + NULL, + NULL ); + GtkWidget* label = gtk_label_new( NULL ); + gtk_widget_set_valign( label, GTK_ALIGN_CENTER ); + gtk_expression_bind( label_expr, label, "label", NULL ); + gtk_box_append( GTK_BOX( box ), label ); + } + + return box; + } + case FOOBAR_STATUS_ITEM_AUDIO: + { + GtkWidget* box = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); + gtk_widget_set_valign( box, GTK_ALIGN_FILL ); + + if ( enable_scrolling ) + { + GtkEventController* scroll_controller = gtk_event_controller_scroll_new( + GTK_EVENT_CONTROLLER_SCROLL_VERTICAL | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE ); + g_signal_connect( + scroll_controller, + "scroll", + G_CALLBACK( foobar_panel_item_status_handle_audio_scroll ), + self ); + gtk_widget_add_controller( box, scroll_controller ); + } + + GtkExpression* device_expr = gtk_constant_expression_new( + FOOBAR_TYPE_AUDIO_DEVICE, + foobar_audio_service_get_default_output( self->audio_service ) ); + GtkExpression* available_expr = gtk_property_expression_new( + FOOBAR_TYPE_AUDIO_DEVICE, + gtk_expression_ref( device_expr ), + "is-available" ); + GtkExpression* volume_expr = gtk_property_expression_new( + FOOBAR_TYPE_AUDIO_DEVICE, + gtk_expression_ref( device_expr ), + "volume" ); + + GtkExpression* icon_params[] = { available_expr, volume_expr }; + GtkExpression* icon_expr = gtk_cclosure_expression_new( + G_TYPE_STRING, + NULL, + G_N_ELEMENTS( icon_params ), + icon_params, + G_CALLBACK( foobar_panel_item_status_compute_audio_icon ), + NULL, + NULL ); + GtkWidget* icon = gtk_image_new( ); + gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); + gtk_expression_bind( icon_expr, icon, "icon-name", NULL ); + gtk_box_append( GTK_BOX( box ), icon ); + + if ( show_labels ) + { + GtkExpression* label_params[] = { gtk_expression_ref( available_expr ), gtk_expression_ref( volume_expr ) }; + GtkExpression* label_expr = gtk_cclosure_expression_new( + G_TYPE_STRING, + NULL, + G_N_ELEMENTS( label_params ), + label_params, + G_CALLBACK( foobar_panel_item_status_compute_audio_label ), + NULL, + NULL ); + GtkWidget* label = gtk_label_new( NULL ); + gtk_widget_set_valign( label, GTK_ALIGN_CENTER ); + gtk_expression_bind( label_expr, label, "label", NULL ); + gtk_box_append( GTK_BOX( box ), label ); + } + + return box; + } + case FOOBAR_STATUS_ITEM_NOTIFICATIONS: + { + GtkWidget* icon = gtk_image_new( ); + gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); + + GtkExpression* list_expr = gtk_constant_expression_new( + G_TYPE_LIST_MODEL, + foobar_notification_service_get_notifications( self->notification_service ) ); + GtkExpression* count_expr = gtk_property_expression_new( + GTK_TYPE_SORT_LIST_MODEL, + list_expr, + "n-items" ); + GtkExpression* icon_params[] = { count_expr }; + GtkExpression* icon_expr = gtk_cclosure_expression_new( + G_TYPE_STRING, + NULL, + G_N_ELEMENTS( icon_params ), + icon_params, + G_CALLBACK( foobar_panel_item_status_compute_notifications_icon ), + NULL, + NULL ); + gtk_expression_bind( icon_expr, icon, "icon-name", NULL ); + + return icon; + } + default: + g_warn_if_reached( ); + return NULL; + } +} + +// +// Force re-evaluation of the network icon expression. +// +// This is used whenever the active network or its strength changes. +// +void foobar_panel_item_status_reevaluate_network( FoobarPanelItemStatus* self ) +{ + if ( self->network_icon && self->network_icon_expr ) + { + GValue value = G_VALUE_INIT; + if ( gtk_expression_evaluate( self->network_icon_expr, NULL, &value ) ) + { + gtk_image_set_from_icon_name( GTK_IMAGE( self->network_icon ), g_value_get_string( &value ) ); + } + g_value_unset( &value ); + } +}
\ No newline at end of file diff --git a/src/widgets/panel/panel-item-status.h b/src/widgets/panel/panel-item-status.h new file mode 100644 index 0000000..e18c097 --- /dev/null +++ b/src/widgets/panel/panel-item-status.h @@ -0,0 +1,25 @@ +#pragma once + +#include "widgets/panel/panel-item.h" +#include "services/battery-service.h" +#include "services/brightness-service.h" +#include "services/audio-service.h" +#include "services/network-service.h" +#include "services/bluetooth-service.h" +#include "services/notification-service.h" + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_PANEL_ITEM_STATUS foobar_panel_item_status_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarPanelItemStatus, foobar_panel_item_status, FOOBAR, PANEL_ITEM_STATUS, FoobarPanelItem ) + +FoobarPanelItem* foobar_panel_item_status_new( FoobarPanelItemConfiguration const* config, + FoobarBatteryService* battery_service, + FoobarBrightnessService* brightness_service, + FoobarAudioService* audio_service, + FoobarNetworkService* network_service, + FoobarBluetoothService* bluetooth_service, + FoobarNotificationService* notification_service ); + +G_END_DECLS
\ No newline at end of file diff --git a/src/widgets/panel/panel-item-workspaces.c b/src/widgets/panel/panel-item-workspaces.c new file mode 100644 index 0000000..acdd201 --- /dev/null +++ b/src/widgets/panel/panel-item-workspaces.c @@ -0,0 +1,327 @@ +#include "widgets/panel/panel-item-workspaces.h" +#include "widgets/inset-container.h" + +// +// FoobarPanelItemWorkspaces: +// +// A panel item showing all workspaces for the current monitor (if multi-monitor mode is enabled) as buttons, indicating +// the current state of the workspace (i.e., whether it's active, visible, special, etc.). +// + +struct _FoobarPanelItemWorkspaces +{ + FoobarPanelItem parent_instance; + GtkWidget* list_view; + GtkWidget* inset_container; + FoobarWorkspaceService* workspace_service; + gint button_size; + gint spacing; + GtkFilter* monitor_filter; + gulong monitor_configuration_handler_id; +}; + +enum +{ + PROP_ORIENTATION = 1, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_panel_item_workspaces_class_init ( FoobarPanelItemWorkspacesClass* klass ); +static void foobar_panel_item_workspaces_init ( FoobarPanelItemWorkspaces* self ); +static void foobar_panel_item_workspaces_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_panel_item_workspaces_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_panel_item_workspaces_finalize ( GObject* object ); +static void foobar_panel_item_workspaces_handle_item_setup ( GtkListItemFactory* factory, + GtkListItem* list_item, + gpointer userdata ); +static void foobar_panel_item_workspaces_handle_item_clicked ( GtkButton* button, + gpointer userdata ); +static void foobar_panel_item_workspaces_handle_monitor_configuration_changed( FoobarWorkspaceService* service, + gpointer userdata ); +static gchar** foobar_panel_item_workspaces_compute_css_classes ( GtkExpression* expression, + FoobarWorkspaceFlags flags, + gpointer userdata ); +static gboolean foobar_panel_item_workspaces_monitor_filter_func ( gpointer item, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE_WITH_CODE( + FoobarPanelItemWorkspaces, + foobar_panel_item_workspaces, + FOOBAR_TYPE_PANEL_ITEM, + G_IMPLEMENT_INTERFACE( GTK_TYPE_ORIENTABLE, NULL ) ) + +// --------------------------------------------------------------------------------------------------------------------- +// Panel Item Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for workspace panel items. +// +void foobar_panel_item_workspaces_class_init( FoobarPanelItemWorkspacesClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_panel_item_workspaces_get_property; + object_klass->set_property = foobar_panel_item_workspaces_set_property; + object_klass->finalize = foobar_panel_item_workspaces_finalize; + + gpointer orientable_iface = g_type_default_interface_peek( GTK_TYPE_ORIENTABLE ); + props[PROP_ORIENTATION] = g_param_spec_override( + "orientation", + g_object_interface_find_property( orientable_iface, "orientation" ) ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for workspace panel items. +// +void foobar_panel_item_workspaces_init( FoobarPanelItemWorkspaces* self ) +{ + GtkListItemFactory* item_factory = gtk_signal_list_item_factory_new( ); + g_signal_connect( item_factory, "setup", G_CALLBACK( foobar_panel_item_workspaces_handle_item_setup ), self ); + + self->list_view = gtk_list_view_new( NULL, item_factory ); + gtk_widget_set_halign( self->list_view, GTK_ALIGN_CENTER ); + gtk_widget_set_valign( self->list_view, GTK_ALIGN_CENTER ); + gtk_list_view_set_single_click_activate( GTK_LIST_VIEW( self->list_view ), TRUE ); + + self->inset_container = foobar_inset_container_new( ); + foobar_inset_container_set_child( FOOBAR_INSET_CONTAINER( self->inset_container ), GTK_WIDGET( self->list_view ) ); + + gtk_widget_add_css_class( GTK_WIDGET( self ), "workspaces" ); + foobar_panel_item_set_child( FOOBAR_PANEL_ITEM( self ), GTK_WIDGET( self->inset_container ) ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_panel_item_workspaces_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarPanelItemWorkspaces* self = (FoobarPanelItemWorkspaces*)object; + + switch ( prop_id ) + { + case PROP_ORIENTATION: + g_value_set_enum( value, gtk_orientable_get_orientation( GTK_ORIENTABLE( self->list_view ) ) ); + 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_panel_item_workspaces_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarPanelItemWorkspaces* self = (FoobarPanelItemWorkspaces*)object; + + switch ( prop_id ) + { + case PROP_ORIENTATION: + gtk_orientable_set_orientation( GTK_ORIENTABLE( self->list_view ), g_value_get_enum( value ) ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ORIENTATION] ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for workspace panel items. +// +void foobar_panel_item_workspaces_finalize( GObject* object ) +{ + FoobarPanelItemWorkspaces* self = (FoobarPanelItemWorkspaces*)object; + + g_clear_signal_handler( &self->monitor_configuration_handler_id, self->workspace_service ); + g_clear_object( &self->workspace_service ); + g_clear_object( &self->monitor_filter ); + + G_OBJECT_CLASS( foobar_panel_item_workspaces_parent_class )->finalize( object ); +} + + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new workspaces panel item with the given configuration. +// +// If monitor is not NULL, only workspaces on this monitor will be shown. +// +FoobarPanelItem* foobar_panel_item_workspaces_new( + FoobarPanelItemConfiguration const* config, + GdkMonitor* monitor, + FoobarWorkspaceService* workspace_service ) +{ + g_return_val_if_fail( config != NULL, NULL ); + g_return_val_if_fail( foobar_panel_item_configuration_get_kind( config ) == FOOBAR_PANEL_ITEM_KIND_WORKSPACES, NULL ); + g_return_val_if_fail( monitor == NULL || GDK_IS_MONITOR( monitor ), NULL ); + g_return_val_if_fail( FOOBAR_IS_WORKSPACE_SERVICE( workspace_service ), NULL ); + + FoobarPanelItemWorkspaces* self = g_object_new( FOOBAR_TYPE_PANEL_ITEM_WORKSPACES, NULL ); + self->button_size = foobar_panel_item_workspaces_configuration_get_button_size( config ); + self->spacing = foobar_panel_item_workspaces_configuration_get_spacing( config ); + self->workspace_service = g_object_ref( workspace_service ); + + // Set up the source model, optionally enabling filtering. + + GListModel* source_model = g_object_ref( foobar_workspace_service_get_workspaces( self->workspace_service ) ); + if ( monitor ) + { + GtkCustomFilter* filter = gtk_custom_filter_new( + foobar_panel_item_workspaces_monitor_filter_func, + g_object_ref( monitor ), + g_object_unref ); + self->monitor_filter = GTK_FILTER( filter ); + source_model = G_LIST_MODEL( gtk_filter_list_model_new( source_model, g_object_ref( self->monitor_filter ) ) ); + + self->monitor_configuration_handler_id = g_signal_connect( + self->workspace_service, + "monitor-configuration-changed", + G_CALLBACK( foobar_panel_item_workspaces_handle_monitor_configuration_changed ), + self ); + } + GtkNoSelection* selection_model = gtk_no_selection_new( source_model ); + gtk_list_view_set_model( GTK_LIST_VIEW( self->list_view ), GTK_SELECTION_MODEL( selection_model ) ); + + // Wrap the list in an inset container to remove the outer margin from the items. + + foobar_inset_container_set_inset_horizontal( FOOBAR_INSET_CONTAINER( self->inset_container ), -self->spacing / 2 ); + foobar_inset_container_set_inset_vertical( FOOBAR_INSET_CONTAINER( self->inset_container ), -self->spacing / 2 ); + + return FOOBAR_PANEL_ITEM( self ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called by the list view to create a widget for displaying a workspace. +// +void foobar_panel_item_workspaces_handle_item_setup( + GtkListItemFactory* factory, + GtkListItem* list_item, + gpointer userdata ) +{ + (void)factory; + FoobarPanelItemWorkspaces* self = (FoobarPanelItemWorkspaces*)userdata; + + GtkWidget* button = gtk_button_new( ); + gtk_widget_set_size_request( button, self->button_size, self->button_size ); + gtk_widget_set_margin_top( button, self->spacing / 2 ); + gtk_widget_set_margin_bottom( button, self->spacing / 2 ); + gtk_widget_set_margin_start( button, self->spacing / 2 ); + gtk_widget_set_margin_end( button, self->spacing / 2 ); + g_signal_connect( + button, + "clicked", + G_CALLBACK( foobar_panel_item_workspaces_handle_item_clicked ), + list_item ); + + gtk_list_item_set_child( list_item, button ); + + { + GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" ); + GtkExpression* active_expr = gtk_property_expression_new( FOOBAR_TYPE_WORKSPACE, item_expr, "flags" ); + GtkExpression* params[] = { active_expr }; + GtkExpression* css_expr = gtk_cclosure_expression_new( + G_TYPE_STRV, + NULL, + G_N_ELEMENTS( params ), + params, + G_CALLBACK( foobar_panel_item_workspaces_compute_css_classes ), + NULL, + NULL ); + gtk_expression_bind( css_expr, button, "css-classes", list_item ); + } +} + +// +// Called by the workspace service when the monitor for any workspace has changed. +// +// This is needed because the GtkFilterListModel does not track changes to the item properties and needs to be +// re-evaluated manually. +// +void foobar_panel_item_workspaces_handle_monitor_configuration_changed( + FoobarWorkspaceService* service, + gpointer userdata ) +{ + (void)service; + FoobarPanelItemWorkspaces* self = (FoobarPanelItemWorkspaces*)userdata; + + gtk_filter_changed( self->monitor_filter, GTK_FILTER_CHANGE_DIFFERENT ); +} + +// +// Called when a workspace button was clicked, activating that workspace. +// +void foobar_panel_item_workspaces_handle_item_clicked( GtkButton* button, gpointer userdata ) +{ + (void)button; + GtkListItem* item = (GtkListItem*)userdata; + + FoobarWorkspace* workspace = gtk_list_item_get_item( item ); + if ( workspace ) { foobar_workspace_activate( workspace ); } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Value Converters +// --------------------------------------------------------------------------------------------------------------------- + +// +// Derive the list of CSS classes for a workspace button from the workspace's current flags. +// +gchar** foobar_panel_item_workspaces_compute_css_classes( + GtkExpression* expression, + FoobarWorkspaceFlags flags, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + GStrvBuilder* builder = g_strv_builder_new( ); + if ( flags & FOOBAR_WORKSPACE_FLAGS_ACTIVE ) { g_strv_builder_add( builder, "active" ); } + if ( flags & FOOBAR_WORKSPACE_FLAGS_VISIBLE ) { g_strv_builder_add( builder, "visible" ); } + if ( flags & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) { g_strv_builder_add( builder, "special" ); } + if ( flags & FOOBAR_WORKSPACE_FLAGS_URGENT ) { g_strv_builder_add( builder, "urgent" ); } + if ( flags & FOOBAR_WORKSPACE_FLAGS_PERSISTENT ) { g_strv_builder_add( builder, "persistent" ); } + return g_strv_builder_end( builder ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Check if a workspace is on the panel's monitor. +// +gboolean foobar_panel_item_workspaces_monitor_filter_func( gpointer item, gpointer userdata ) +{ + FoobarWorkspace* workspace = (FoobarWorkspace*)item; + GdkMonitor* monitor = (GdkMonitor*)userdata; + + gchar const* expected = gdk_monitor_get_connector( monitor ); + gchar const* actual = foobar_workspace_get_monitor( workspace ); + return !expected || !g_strcmp0( expected, actual ); +}
\ No newline at end of file diff --git a/src/widgets/panel/panel-item-workspaces.h b/src/widgets/panel/panel-item-workspaces.h new file mode 100644 index 0000000..c9d7ef1 --- /dev/null +++ b/src/widgets/panel/panel-item-workspaces.h @@ -0,0 +1,16 @@ +#pragma once + +#include "widgets/panel/panel-item.h" +#include "services/workspace-service.h" + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_PANEL_ITEM_WORKSPACES foobar_panel_item_workspaces_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarPanelItemWorkspaces, foobar_panel_item_workspaces, FOOBAR, PANEL_ITEM_WORKSPACES, FoobarPanelItem ) + +FoobarPanelItem* foobar_panel_item_workspaces_new( FoobarPanelItemConfiguration const* config, + GdkMonitor* monitor, + FoobarWorkspaceService* workspace_service ); + +G_END_DECLS
\ No newline at end of file diff --git a/src/widgets/panel/panel-item.c b/src/widgets/panel/panel-item.c new file mode 100644 index 0000000..f2d4383 --- /dev/null +++ b/src/widgets/panel/panel-item.c @@ -0,0 +1,195 @@ +#include "widgets/panel/panel-item.h" +#include "application.h" + +// +// FoobarPanelItem: +// +// Base class for all panel items. This is basically a bin layout where the subclass usually sets the child widget. +// + +typedef struct _FoobarPanelItemPrivate +{ + GtkWidget* child; +} FoobarPanelItemPrivate; + +enum +{ + PROP_CHILD = 1, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_panel_item_class_init ( FoobarPanelItemClass* klass ); +static void foobar_panel_item_init ( FoobarPanelItem* self ); +static void foobar_panel_item_get_property( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_panel_item_set_property( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_panel_item_dispose ( GObject* object ); + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE( FoobarPanelItem, foobar_panel_item, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for panel items. +// +void foobar_panel_item_class_init( FoobarPanelItemClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + gtk_widget_class_set_css_name( widget_klass, "panel-item" ); + gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BIN_LAYOUT ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_panel_item_get_property; + object_klass->set_property = foobar_panel_item_set_property; + object_klass->dispose = foobar_panel_item_dispose; + + props[PROP_CHILD] = g_param_spec_object( + "child", + "Child", + "Child widget for the panel item.", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for panel items. +// +void foobar_panel_item_init( FoobarPanelItem* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_panel_item_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarPanelItem* self = (FoobarPanelItem*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + g_value_set_object( value, foobar_panel_item_get_child( 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_panel_item_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarPanelItem* self = (FoobarPanelItem*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + foobar_panel_item_set_child( self, g_value_get_object( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for panel items. +// +void foobar_panel_item_dispose( GObject* object ) +{ + FoobarPanelItem* self = (FoobarPanelItem*)object; + FoobarPanelItemPrivate* priv = foobar_panel_item_get_instance_private( self ); + + g_clear_pointer( &priv->child, gtk_widget_unparent ); + + G_OBJECT_CLASS( foobar_panel_item_parent_class )->dispose( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Get the child widget for the panel item. +// +GtkWidget* foobar_panel_item_get_child( FoobarPanelItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_PANEL_ITEM( self ), NULL ); + FoobarPanelItemPrivate* priv = foobar_panel_item_get_instance_private( self ); + + return priv->child; +} + +// +// Update the child widget for the panel item. +// +void foobar_panel_item_set_child( + FoobarPanelItem* self, + GtkWidget* child ) +{ + g_return_if_fail( FOOBAR_IS_PANEL_ITEM( self ) ); + FoobarPanelItemPrivate* priv = foobar_panel_item_get_instance_private( self ); + g_return_if_fail( child == NULL || priv->child == child || gtk_widget_get_parent( child ) == NULL ); + + if ( priv->child != child ) + { + g_clear_pointer( &priv->child, gtk_widget_unparent ); + + if ( child ) + { + priv->child = child; + gtk_widget_set_parent( child, GTK_WIDGET( self ) ); + } + + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] ); + } +} + +// +// Invoke the action for the panel item. +// +// This needs to be triggered by a subclass, for example by adding a button to the item. Support for panel item actions +// is opt-in and subclasses have to explicitly implement it. This is just a helper for implementing the action. +// +void foobar_panel_item_invoke_action( + FoobarPanelItem* self, + FoobarPanelItemAction action ) +{ + g_return_if_fail( FOOBAR_IS_PANEL_ITEM( self ) ); + + switch ( action ) + { + case FOOBAR_PANEL_ITEM_ACTION_NONE: + break; + case FOOBAR_PANEL_ITEM_ACTION_LAUNCHER: + foobar_application_toggle_launcher( FOOBAR_APPLICATION_DEFAULT ); + break; + case FOOBAR_PANEL_ITEM_ACTION_CONTROL_CENTER: + foobar_application_toggle_control_center( FOOBAR_APPLICATION_DEFAULT ); + break; + default: + g_warn_if_reached( ); + break; + } +}
\ No newline at end of file diff --git a/src/widgets/panel/panel-item.h b/src/widgets/panel/panel-item.h new file mode 100644 index 0000000..318bd85 --- /dev/null +++ b/src/widgets/panel/panel-item.h @@ -0,0 +1,23 @@ +#pragma once + +#include <gtk/gtk.h> +#include "services/configuration-service.h" + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_PANEL_ITEM foobar_panel_item_get_type( ) + +G_DECLARE_DERIVABLE_TYPE( FoobarPanelItem, foobar_panel_item, FOOBAR, PANEL_ITEM, GtkWidget ) + +struct _FoobarPanelItemClass +{ + GtkWidgetClass parent_class; +}; + +GtkWidget* foobar_panel_item_get_child ( FoobarPanelItem* self ); +void foobar_panel_item_set_child ( FoobarPanelItem* self, + GtkWidget* child ); +void foobar_panel_item_invoke_action( FoobarPanelItem* self, + FoobarPanelItemAction action ); + +G_END_DECLS
\ No newline at end of file |
