From 37d5ef75306c45ca95550f4af40d9750c5b9cb88 Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Mon, 6 May 2024 13:42:55 +0200 Subject: initial commit yup, i definitely wrote all of this in a single commit --- src/widgets/panel/meson.build | 7 + src/widgets/panel/panel-item-clock.c | 152 +++++ src/widgets/panel/panel-item-clock.h | 15 + src/widgets/panel/panel-item-icon.c | 92 +++ src/widgets/panel/panel-item-icon.h | 13 + src/widgets/panel/panel-item-status.c | 955 ++++++++++++++++++++++++++++++ src/widgets/panel/panel-item-status.h | 25 + src/widgets/panel/panel-item-workspaces.c | 327 ++++++++++ src/widgets/panel/panel-item-workspaces.h | 16 + src/widgets/panel/panel-item.c | 195 ++++++ src/widgets/panel/panel-item.h | 23 + 11 files changed, 1820 insertions(+) create mode 100644 src/widgets/panel/meson.build create mode 100644 src/widgets/panel/panel-item-clock.c create mode 100644 src/widgets/panel/panel-item-clock.h create mode 100644 src/widgets/panel/panel-item-icon.c create mode 100644 src/widgets/panel/panel-item-icon.h create mode 100644 src/widgets/panel/panel-item-status.c create mode 100644 src/widgets/panel/panel-item-status.h create mode 100644 src/widgets/panel/panel-item-workspaces.c create mode 100644 src/widgets/panel/panel-item-workspaces.h create mode 100644 src/widgets/panel/panel-item.c create mode 100644 src/widgets/panel/panel-item.h (limited to 'src/widgets/panel') 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 +#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 -- cgit v1.3.1