summaryrefslogtreecommitdiff
path: root/src/widgets
diff options
context:
space:
mode:
Diffstat (limited to 'src/widgets')
-rw-r--r--src/widgets/control-center/control-button.c496
-rw-r--r--src/widgets/control-center/control-button.h31
-rw-r--r--src/widgets/control-center/control-details-item.c307
-rw-r--r--src/widgets/control-center/control-details-item.h32
-rw-r--r--src/widgets/control-center/control-details.c289
-rw-r--r--src/widgets/control-center/control-details.h22
-rw-r--r--src/widgets/control-center/control-slider.c428
-rw-r--r--src/widgets/control-center/control-slider.h28
-rw-r--r--src/widgets/control-center/meson.build6
-rw-r--r--src/widgets/inset-container.c363
-rw-r--r--src/widgets/inset-container.h22
-rw-r--r--src/widgets/limit-container.c341
-rw-r--r--src/widgets/limit-container.h22
-rw-r--r--src/widgets/meson.build8
-rw-r--r--src/widgets/notification-widget.c821
-rw-r--r--src/widgets/notification-widget.h50
-rw-r--r--src/widgets/panel/meson.build7
-rw-r--r--src/widgets/panel/panel-item-clock.c152
-rw-r--r--src/widgets/panel/panel-item-clock.h15
-rw-r--r--src/widgets/panel/panel-item-icon.c92
-rw-r--r--src/widgets/panel/panel-item-icon.h13
-rw-r--r--src/widgets/panel/panel-item-status.c955
-rw-r--r--src/widgets/panel/panel-item-status.h25
-rw-r--r--src/widgets/panel/panel-item-workspaces.c327
-rw-r--r--src/widgets/panel/panel-item-workspaces.h16
-rw-r--r--src/widgets/panel/panel-item.c195
-rw-r--r--src/widgets/panel/panel-item.h23
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