summaryrefslogtreecommitdiff
path: root/csrc/control-center.c
diff options
context:
space:
mode:
authorHannes Schulze <haschu0103@outlook.de>2025-06-19 23:17:58 +0200
committerHannes Schulze <haschu0103@outlook.de>2025-06-19 23:17:58 +0200
commit1af3d14f3f995645a0de08a54b244c19840709e9 (patch)
tree3c4098863d8a8fd004a2acc8912a23b6036ab4dd /csrc/control-center.c
parent40efcf60a5f39674b5137f11de7bcfe37fb121b5 (diff)
init hs project
Diffstat (limited to 'csrc/control-center.c')
-rw-r--r--csrc/control-center.c821
1 files changed, 821 insertions, 0 deletions
diff --git a/csrc/control-center.c b/csrc/control-center.c
new file mode 100644
index 0000000..bb22830
--- /dev/null
+++ b/csrc/control-center.c
@@ -0,0 +1,821 @@
+#include "control-center.h"
+#include "widgets/control-center/control-button.h"
+#include "widgets/control-center/control-slider.h"
+#include "widgets/control-center/control-details.h"
+#include "widgets/control-center/control-details-item.h"
+#include "widgets/inset-container.h"
+#include "widgets/notification-widget.h"
+#include <gtk4-layer-shell.h>
+
+#define STACK_ITEM_LIST "list"
+#define STACK_ITEM_PLACEHOLDER "placeholder"
+
+//
+// FoobarControlCenter:
+//
+// The control center is made up of two sections:
+// - The "controls" section for configuring wi-fi, volume, brightness, etc.
+// - The "notifications" section showing all notifications (including dismissed ones) and allowing the user to close
+// them.
+//
+
+struct _FoobarControlCenter
+{
+ GtkWindow parent_instance;
+ GtkWidget* control_container;
+ GtkWidget* notification_list;
+ GtkWidget* notification_container;
+ GtkWidget* notification_placeholder;
+ GtkWidget* notification_stack;
+ GtkWidget* layout;
+ FoobarBrightnessService* brightness_service;
+ FoobarAudioService* audio_service;
+ FoobarNetworkService* network_service;
+ FoobarBluetoothService* bluetooth_service;
+ FoobarNotificationService* notification_service;
+ FoobarConfigurationService* configuration_service;
+ gchar* notification_time_format;
+ gint notification_min_height;
+ gint notification_close_button_inset;
+ gint padding;
+ gint spacing;
+ gulong config_handler_id;
+};
+
+static void foobar_control_center_class_init ( FoobarControlCenterClass* klass );
+static void foobar_control_center_init ( FoobarControlCenter* self );
+static void foobar_control_center_finalize ( GObject* object );
+static void foobar_control_center_handle_notification_setup ( GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata );
+static void foobar_control_center_handle_network_setup ( GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata );
+static void foobar_control_center_handle_bluetooth_device_setup ( GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata );
+static void foobar_control_center_handle_bluetooth_device_activate ( GtkListView* view,
+ guint position,
+ gpointer userdata );
+static void foobar_control_center_handle_audio_device_setup ( GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata );
+static void foobar_control_center_handle_audio_device_activate ( GtkListView* view,
+ guint position,
+ gpointer userdata );
+static void foobar_control_center_handle_config_change ( GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata );
+static FoobarControlDetailsAccessory foobar_control_center_compute_bluetooth_device_accessory ( GtkExpression* expression,
+ FoobarBluetoothDeviceState state,
+ gpointer userdata );
+static gchar* foobar_control_center_compute_visible_notification_child_name( GtkExpression* expression,
+ guint count,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarControlCenter, foobar_control_center, GTK_TYPE_WINDOW )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Window Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for the control center.
+//
+void foobar_control_center_class_init( FoobarControlCenterClass* klass )
+{
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->finalize = foobar_control_center_finalize;
+}
+
+//
+// Instance initialization for the control center.
+//
+void foobar_control_center_init( FoobarControlCenter* self )
+{
+ self->control_container = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_widget_add_css_class( self->control_container, "controls" );
+ gtk_widget_set_hexpand( self->control_container, TRUE );
+
+ GtkListItemFactory* notification_factory = gtk_signal_list_item_factory_new( );
+ g_signal_connect(
+ notification_factory,
+ "setup",
+ G_CALLBACK( foobar_control_center_handle_notification_setup ),
+ self );
+ self->notification_list = gtk_list_view_new( NULL, notification_factory );
+ gtk_widget_add_css_class( self->notification_list, "notifications" );
+
+ // XXX: Get rid of this container to enable virtualization; currently needed because I haven't figured out how to
+ // set padding without using css (setting margin on list view causes glitches when scrolling).
+ self->notification_container = foobar_inset_container_new( );
+ foobar_inset_container_set_child( FOOBAR_INSET_CONTAINER( self->notification_container ), self->notification_list );
+
+ GtkWidget* scrolled_window = gtk_scrolled_window_new( );
+ gtk_scrolled_window_set_child( GTK_SCROLLED_WINDOW( scrolled_window ), self->notification_container );
+ gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scrolled_window ), GTK_POLICY_NEVER, GTK_POLICY_EXTERNAL );
+
+ self->notification_placeholder = gtk_label_new( "No Notifications" );
+ gtk_widget_add_css_class( self->notification_placeholder, "placeholder" );
+ gtk_label_set_ellipsize( GTK_LABEL( self->notification_placeholder ), PANGO_ELLIPSIZE_END );
+ gtk_label_set_wrap( GTK_LABEL( self->notification_placeholder ), FALSE );
+
+ self->notification_stack = gtk_stack_new( );
+ gtk_stack_add_named( GTK_STACK( self->notification_stack ), scrolled_window, STACK_ITEM_LIST );
+ gtk_stack_add_named( GTK_STACK( self->notification_stack ), self->notification_placeholder, STACK_ITEM_PLACEHOLDER );
+ gtk_widget_set_vexpand( self->notification_stack, TRUE );
+ gtk_widget_set_hexpand( self->notification_stack, TRUE );
+
+ self->layout = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_box_append( GTK_BOX( self->layout ), self->control_container );
+ gtk_box_append( GTK_BOX( self->layout ), gtk_separator_new( GTK_ORIENTATION_HORIZONTAL ) );
+ gtk_box_append( GTK_BOX( self->layout ), self->notification_stack );
+
+ g_autoptr( GtkSizeGroup ) size_group = gtk_size_group_new( GTK_SIZE_GROUP_HORIZONTAL );
+ gtk_size_group_add_widget( size_group, self->control_container );
+ gtk_size_group_add_widget( size_group, self->notification_stack );
+
+ gtk_window_set_child( GTK_WINDOW( self ), self->layout );
+ gtk_window_set_title( GTK_WINDOW( self ), "Foobar Control Center" );
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "control-center" );
+ gtk_layer_init_for_window( GTK_WINDOW( self ) );
+ gtk_layer_set_namespace( GTK_WINDOW( self ), "foobar-control-center" );
+}
+
+//
+// Instance cleanup for the control center.
+//
+void foobar_control_center_finalize( GObject* object )
+{
+ FoobarControlCenter* self = (FoobarControlCenter*)object;
+
+ g_clear_signal_handler( &self->config_handler_id, self->configuration_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->configuration_service );
+ g_clear_object( &self->notification_service );
+ g_clear_pointer( &self->notification_time_format, g_free );
+
+ G_OBJECT_CLASS( foobar_control_center_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new control center instance.
+//
+FoobarControlCenter* foobar_control_center_new(
+ FoobarBrightnessService* brightness_service,
+ FoobarAudioService* audio_service,
+ FoobarNetworkService* network_service,
+ FoobarBluetoothService* bluetooth_service,
+ FoobarNotificationService* notification_service,
+ FoobarConfigurationService* configuration_service )
+{
+ 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 );
+ g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( configuration_service ), NULL );
+
+ FoobarControlCenter* self = g_object_new( FOOBAR_TYPE_CONTROL_CENTER, NULL );
+ 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 );
+ self->configuration_service = g_object_ref( configuration_service );
+
+ // Apply the configuration and subscribe to changes.
+
+ FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service );
+ foobar_control_center_apply_configuration(
+ self,
+ foobar_configuration_get_control_center( config ),
+ foobar_configuration_get_notifications( config ) );
+ self->config_handler_id = g_signal_connect(
+ self->configuration_service,
+ "notify::current",
+ G_CALLBACK( foobar_control_center_handle_config_change ),
+ self );
+
+ // Set up the notifications list view.
+
+ GListModel* source_model = foobar_notification_service_get_notifications( self->notification_service );
+ GtkNoSelection* selection_model = gtk_no_selection_new( g_object_ref( source_model ) );
+ gtk_list_view_set_model( GTK_LIST_VIEW( self->notification_list ), GTK_SELECTION_MODEL( selection_model ) );
+
+ // Set up bindings.
+
+ {
+ GtkExpression* count_expr = gtk_property_expression_new( GTK_TYPE_SORT_LIST_MODEL, NULL, "n-items" );
+ GtkExpression* child_params[] = { count_expr };
+ GtkExpression* child_expr = gtk_cclosure_expression_new(
+ G_TYPE_STRING,
+ NULL,
+ G_N_ELEMENTS( child_params ),
+ child_params,
+ G_CALLBACK( foobar_control_center_compute_visible_notification_child_name ),
+ NULL,
+ NULL );
+ gtk_expression_bind( child_expr, self->notification_stack, "visible-child-name", source_model );
+ }
+
+ return self;
+}
+
+//
+// Apply the control center configuration provided by the configuration service, additionally using the provided
+// notification configuration for some settings.
+//
+void foobar_control_center_apply_configuration(
+ FoobarControlCenter* self,
+ FoobarControlCenterConfiguration const* config,
+ FoobarNotificationConfiguration const* notification_config )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_CENTER( self ) );
+ g_return_if_fail( config != NULL );
+ g_return_if_fail( notification_config != NULL );
+
+ // Copy configuration into member variables.
+
+ g_clear_pointer( &self->notification_time_format, g_free );
+ self->notification_time_format = g_strdup( foobar_notification_configuration_get_time_format( notification_config ) );
+ self->notification_min_height = foobar_notification_configuration_get_min_height( notification_config );
+ self->notification_close_button_inset = foobar_notification_configuration_get_close_button_inset( notification_config );
+ self->padding = foobar_control_center_configuration_get_padding( config );
+ self->spacing = foobar_control_center_configuration_get_spacing( config );
+
+ // Configure the window.
+
+ gtk_window_set_default_size(
+ GTK_WINDOW( self ),
+ foobar_control_center_configuration_get_width( config ),
+ foobar_control_center_configuration_get_height( config ) );
+
+ FoobarScreenEdge position = foobar_control_center_configuration_get_position( config );
+ FoobarControlCenterAlignment alignment = foobar_control_center_configuration_get_alignment( config );
+ gboolean attach_left = ( position == FOOBAR_SCREEN_EDGE_LEFT );
+ gboolean attach_right = ( position == FOOBAR_SCREEN_EDGE_RIGHT );
+ gboolean attach_top = ( position == FOOBAR_SCREEN_EDGE_TOP );
+ gboolean attach_bottom = ( position == FOOBAR_SCREEN_EDGE_BOTTOM );
+ switch ( position )
+ {
+ case FOOBAR_SCREEN_EDGE_LEFT:
+ case FOOBAR_SCREEN_EDGE_RIGHT:
+ attach_top =
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_START ) ||
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL );
+ attach_bottom =
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_END ) ||
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL );
+ break;
+ case FOOBAR_SCREEN_EDGE_TOP:
+ case FOOBAR_SCREEN_EDGE_BOTTOM:
+ attach_left =
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_START ) ||
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL );
+ attach_right =
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_END ) ||
+ ( alignment == FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_LEFT, attach_left );
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_RIGHT, attach_right );
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, attach_top );
+ gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_BOTTOM, attach_bottom );
+
+ gint offset = foobar_control_center_configuration_get_offset( config );
+ gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_LEFT, attach_left ? offset : 0 );
+ gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_RIGHT, attach_right ? offset : 0 );
+ gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, attach_top ? offset : 0 );
+ gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_BOTTOM, attach_bottom ? offset : 0 );
+
+ // Configure the orientation.
+
+ FoobarOrientation orientation = foobar_control_center_configuration_get_orientation( config );
+ switch ( orientation )
+ {
+ case FOOBAR_ORIENTATION_HORIZONTAL:
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( self->layout ), GTK_ORIENTATION_HORIZONTAL );
+ break;
+ case FOOBAR_ORIENTATION_VERTICAL:
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( self->layout ), GTK_ORIENTATION_VERTICAL );
+ break;
+ default:
+ g_warn_if_reached( );
+ break;
+ }
+
+ // Adjust spacing for the containers.
+
+ foobar_inset_container_set_inset_vertical( FOOBAR_INSET_CONTAINER( self->notification_container ), -self->spacing / 2 );
+ gtk_widget_set_margin_top( self->notification_container, self->padding );
+ gtk_widget_set_margin_bottom( self->notification_container, self->padding );
+
+ gtk_widget_set_margin_start( self->notification_placeholder, self->padding );
+ gtk_widget_set_margin_end( self->notification_placeholder, self->padding );
+ gtk_widget_set_margin_top( self->notification_placeholder, self->padding );
+ gtk_widget_set_margin_bottom( self->notification_placeholder, self->padding );
+
+ gtk_box_set_spacing( GTK_BOX( self->control_container ), self->spacing );
+ gtk_widget_set_margin_top( self->control_container, self->padding );
+ gtk_widget_set_margin_bottom( self->control_container, self->padding );
+
+ // Re-create list items by resetting the factory.
+
+ GtkListItemFactory* factory = gtk_list_view_get_factory( GTK_LIST_VIEW( self->notification_list ) );
+ g_object_ref( factory );
+ gtk_list_view_set_factory( GTK_LIST_VIEW( self->notification_list ), NULL );
+ gtk_list_view_set_factory( GTK_LIST_VIEW( self->notification_list ), factory );
+ g_object_unref( factory );
+
+ // Re-create control rows.
+
+ GtkWidget* row_widget;
+ while ( ( row_widget = gtk_widget_get_first_child( self->control_container ) ) )
+ {
+ gtk_widget_unparent( row_widget );
+ }
+
+ gsize rows_count;
+ FoobarControlCenterRow const* rows = foobar_control_center_configuration_get_rows( config, &rows_count );
+ for ( gsize i = 0; i < rows_count; ++i )
+ {
+ // Add rows. If there are details for a row, add them into another box that is then added to the
+ // control_container to circumvent the spacing of the box.
+
+ switch ( rows[i] )
+ {
+ case FOOBAR_CONTROL_CENTER_ROW_CONNECTIVITY:
+ {
+ // Create the buttons for network and bluetooth.
+
+ GtkWidget* network_button = foobar_control_button_new( );
+
+ GtkWidget* bluetooth_button = foobar_control_button_new( );
+ foobar_control_button_set_icon_name( FOOBAR_CONTROL_BUTTON( bluetooth_button ), "fluent-bluetooth-symbolic" );
+ foobar_control_button_set_label( FOOBAR_CONTROL_BUTTON( bluetooth_button ), "Bluetooth" );
+
+ GtkWidget* button_container = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, self->spacing );
+ gtk_box_set_homogeneous( GTK_BOX( button_container ), TRUE );
+ gtk_widget_set_margin_start( button_container, self->padding );
+ gtk_widget_set_margin_end( button_container, self->padding );
+ gtk_box_append( GTK_BOX( button_container ), network_button );
+ gtk_box_append( GTK_BOX( button_container ), bluetooth_button );
+
+ GtkWidget* row = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_box_append( GTK_BOX( row ), button_container );
+
+ // Show either a button for managing Wi-Fi or a static element for ethernet.
+
+ FoobarNetworkAdapterWifi* wifi_adapter = foobar_network_service_get_wifi( self->network_service );
+ if ( wifi_adapter )
+ {
+ foobar_control_button_set_icon_name( FOOBAR_CONTROL_BUTTON( network_button ), "fluent-wifi-1-symbolic" );
+ foobar_control_button_set_label( FOOBAR_CONTROL_BUTTON( network_button ), "Wi-Fi" );
+ foobar_control_button_set_can_expand( FOOBAR_CONTROL_BUTTON( network_button ), TRUE );
+ foobar_control_button_set_can_toggle( FOOBAR_CONTROL_BUTTON( network_button ), TRUE );
+ g_object_bind_property(
+ wifi_adapter,
+ "is-enabled",
+ network_button,
+ "is-toggled",
+ G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL );
+ g_object_bind_property(
+ network_button,
+ "is-toggled",
+ network_button,
+ "can-expand",
+ G_BINDING_SYNC_CREATE );
+ g_object_bind_property(
+ network_button,
+ "is-expanded",
+ wifi_adapter,
+ "is-scanning",
+ G_BINDING_SYNC_CREATE );
+
+ GtkListItemFactory* wifi_item_factory = gtk_signal_list_item_factory_new( );
+ g_signal_connect(
+ wifi_item_factory,
+ "setup",
+ G_CALLBACK( foobar_control_center_handle_network_setup ),
+ self );
+
+ GListModel* wifi_source_model = g_object_ref( foobar_network_adapter_wifi_get_networks( wifi_adapter ) );
+ GtkNoSelection* wifi_selection_model = gtk_no_selection_new( wifi_source_model );
+
+ GtkWidget* wifi_list_view = gtk_list_view_new( GTK_SELECTION_MODEL( wifi_selection_model ), wifi_item_factory );
+
+ GtkWidget* wifi_details = foobar_control_details_new( );
+ foobar_control_details_set_inset_top( FOOBAR_CONTROL_DETAILS( wifi_details ), self->spacing );
+ foobar_control_details_set_child( FOOBAR_CONTROL_DETAILS( wifi_details ), wifi_list_view );
+ g_object_bind_property(
+ network_button,
+ "is-expanded",
+ wifi_details,
+ "is-expanded",
+ G_BINDING_SYNC_CREATE );
+
+ gtk_box_append( GTK_BOX( row ), wifi_details );
+ }
+ else
+ {
+ foobar_control_button_set_icon_name( FOOBAR_CONTROL_BUTTON( network_button ), "fluent-virtual-network-symbolic" );
+ foobar_control_button_set_label( FOOBAR_CONTROL_BUTTON( network_button ), "Ethernet" );
+ foobar_control_button_set_can_expand( FOOBAR_CONTROL_BUTTON( network_button ), FALSE );
+ foobar_control_button_set_can_toggle( FOOBAR_CONTROL_BUTTON( network_button ), FALSE );
+ }
+
+ // Enable interactions for the bluetooth button if available.
+
+ g_object_bind_property(
+ self->bluetooth_service,
+ "is-available",
+ bluetooth_button,
+ "can-toggle",
+ G_BINDING_SYNC_CREATE );
+ g_object_bind_property(
+ self->bluetooth_service,
+ "is-enabled",
+ bluetooth_button,
+ "is-toggled",
+ G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL );
+ g_object_bind_property(
+ bluetooth_button,
+ "is-toggled",
+ bluetooth_button,
+ "can-expand",
+ G_BINDING_SYNC_CREATE );
+ g_object_bind_property(
+ bluetooth_button,
+ "is-expanded",
+ self->bluetooth_service,
+ "is-scanning",
+ G_BINDING_SYNC_CREATE );
+
+ GtkListItemFactory* bluetooth_item_factory = gtk_signal_list_item_factory_new( );
+ g_signal_connect(
+ bluetooth_item_factory,
+ "setup",
+ G_CALLBACK( foobar_control_center_handle_bluetooth_device_setup ),
+ self );
+
+ GListModel* bluetooth_source_model = g_object_ref( foobar_bluetooth_service_get_devices( self->bluetooth_service ) );
+ GtkNoSelection* bluetooth_selection_model = gtk_no_selection_new( bluetooth_source_model );
+
+ GtkWidget* bluetooth_list_view = gtk_list_view_new(
+ GTK_SELECTION_MODEL( bluetooth_selection_model ),
+ bluetooth_item_factory );
+ gtk_list_view_set_single_click_activate( GTK_LIST_VIEW( bluetooth_list_view ), TRUE );
+ g_signal_connect(
+ bluetooth_list_view,
+ "activate",
+ G_CALLBACK( foobar_control_center_handle_bluetooth_device_activate ),
+ NULL );
+
+ GtkWidget* bluetooth_details = foobar_control_details_new( );
+ foobar_control_details_set_inset_top( FOOBAR_CONTROL_DETAILS( bluetooth_details ), self->spacing );
+ foobar_control_details_set_child( FOOBAR_CONTROL_DETAILS( bluetooth_details ), bluetooth_list_view );
+ g_object_bind_property(
+ bluetooth_button,
+ "is-expanded",
+ bluetooth_details,
+ "is-expanded",
+ G_BINDING_SYNC_CREATE );
+
+ gtk_box_append( GTK_BOX( row ), bluetooth_details );
+
+ gtk_box_append( GTK_BOX( self->control_container ), row );
+ break;
+ }
+ case FOOBAR_CONTROL_CENTER_ROW_AUDIO_OUTPUT:
+ case FOOBAR_CONTROL_CENTER_ROW_AUDIO_INPUT:
+ {
+ FoobarAudioDevice* default_device;
+ GListModel* device_list;
+ gchar const* icon_name;
+ gchar const* label;
+ if ( rows[i] == FOOBAR_CONTROL_CENTER_ROW_AUDIO_OUTPUT )
+ {
+ default_device = foobar_audio_service_get_default_output( self->audio_service );
+ device_list = foobar_audio_service_get_outputs( self->audio_service );
+ icon_name = "fluent-speaker-2-symbolic";
+ label = "Audio Output";
+ }
+ else
+ {
+ default_device = foobar_audio_service_get_default_input( self->audio_service );
+ device_list = foobar_audio_service_get_inputs( self->audio_service );
+ icon_name = "fluent-microphone-symbolic";
+ label = "Audio Input";
+ }
+
+ // Create the slider for controlling the volume.
+
+ GtkWidget* slider = foobar_control_slider_new( );
+ gtk_widget_set_margin_start( slider, self->padding );
+ gtk_widget_set_margin_end( slider, self->padding );
+ foobar_control_slider_set_icon_name( FOOBAR_CONTROL_SLIDER( slider ), icon_name );
+ foobar_control_slider_set_label( FOOBAR_CONTROL_SLIDER( slider ), label );
+ foobar_control_slider_set_can_expand( FOOBAR_CONTROL_SLIDER( slider ), TRUE );
+ g_object_bind_property(
+ default_device,
+ "volume",
+ slider,
+ "percentage",
+ G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL );
+
+ // Create the details view for selecting the audio device.
+
+ GtkListItemFactory* item_factory = gtk_signal_list_item_factory_new( );
+ g_signal_connect( item_factory, "setup", G_CALLBACK( foobar_control_center_handle_audio_device_setup ), self );
+
+ GListModel* source_model = g_object_ref( device_list );
+ GtkNoSelection* selection_model = gtk_no_selection_new( source_model );
+
+ GtkWidget* list_view = gtk_list_view_new( GTK_SELECTION_MODEL( selection_model ), item_factory );
+ gtk_list_view_set_single_click_activate( GTK_LIST_VIEW( list_view ), TRUE );
+ g_signal_connect(
+ list_view,
+ "activate",
+ G_CALLBACK( foobar_control_center_handle_audio_device_activate ),
+ NULL );
+
+ GtkWidget* details = foobar_control_details_new( );
+ foobar_control_details_set_inset_top( FOOBAR_CONTROL_DETAILS( details ), self->spacing );
+ foobar_control_details_set_child( FOOBAR_CONTROL_DETAILS( details ), list_view );
+ g_object_bind_property(
+ slider,
+ "is-expanded",
+ details,
+ "is-expanded",
+ G_BINDING_SYNC_CREATE );
+
+ GtkWidget* row = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_box_append( GTK_BOX( row ), slider );
+ gtk_box_append( GTK_BOX( row ), details );
+
+ gtk_box_append( GTK_BOX( self->control_container ), row );
+ break;
+ }
+ case FOOBAR_CONTROL_CENTER_ROW_BRIGHTNESS:
+ {
+ // Create the slider for adjusting the brightness.
+
+ GtkWidget* slider = foobar_control_slider_new( );
+ gtk_widget_set_margin_start( slider, self->padding );
+ gtk_widget_set_margin_end( slider, self->padding );
+ foobar_control_slider_set_icon_name( FOOBAR_CONTROL_SLIDER( slider ), "fluent-brightness-high-symbolic" );
+ foobar_control_slider_set_label( FOOBAR_CONTROL_SLIDER( slider ), "Brightness" );
+ g_object_bind_property(
+ self->brightness_service,
+ "percentage",
+ slider,
+ "percentage",
+ G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL );
+
+ gtk_box_append( GTK_BOX( self->control_container ), slider );
+ break;
+ }
+ default:
+ {
+ g_warn_if_reached( );
+ break;
+ }
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called by the notification list view to create a widget for displaying a notification.
+//
+void foobar_control_center_handle_notification_setup(
+ GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata )
+{
+ (void)factory;
+ FoobarControlCenter* self = (FoobarControlCenter*)userdata;
+
+ GtkWidget* widget = foobar_notification_widget_new( );
+ foobar_notification_widget_set_close_action( FOOBAR_NOTIFICATION_WIDGET( widget ), FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE );
+ foobar_notification_widget_set_time_format( FOOBAR_NOTIFICATION_WIDGET( widget ), self->notification_time_format );
+ foobar_notification_widget_set_min_height( FOOBAR_NOTIFICATION_WIDGET( widget ), self->notification_min_height );
+ foobar_notification_widget_set_close_button_inset( FOOBAR_NOTIFICATION_WIDGET( widget ), self->notification_close_button_inset );
+ foobar_notification_widget_set_inset_start( FOOBAR_NOTIFICATION_WIDGET( widget ), self->padding );
+ foobar_notification_widget_set_inset_end( FOOBAR_NOTIFICATION_WIDGET( widget ), self->padding );
+ foobar_notification_widget_set_inset_top( FOOBAR_NOTIFICATION_WIDGET( widget ), self->spacing / 2 );
+ foobar_notification_widget_set_inset_bottom( FOOBAR_NOTIFICATION_WIDGET( widget ), self->spacing / 2 );
+ gtk_list_item_set_child( list_item, widget );
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ gtk_expression_bind( item_expr, widget, "notification", list_item );
+ }
+}
+
+//
+// Called by the wi-fi details list view to create a widget for displaying a network.
+//
+void foobar_control_center_handle_network_setup(
+ GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata )
+{
+ (void)factory;
+ FoobarControlCenter* self = (FoobarControlCenter*)userdata;
+
+ GtkWidget* item = foobar_control_details_item_new( );
+ gtk_widget_set_margin_start( item, self->padding );
+ gtk_widget_set_margin_end( item, self->padding );
+ gtk_list_item_set_child( list_item, item );
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* label_expr = gtk_property_expression_new( FOOBAR_TYPE_NETWORK, item_expr, "name" );
+ gtk_expression_bind( label_expr, item, "label", list_item );
+ }
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* checked_expr = gtk_property_expression_new( FOOBAR_TYPE_NETWORK, item_expr, "is-active" );
+ gtk_expression_bind( checked_expr, item, "is-checked", list_item );
+ }
+}
+
+//
+// Called by the bluetooth details list view to create a widget for displaying a device.
+//
+void foobar_control_center_handle_bluetooth_device_setup(
+ GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata )
+{
+ (void)factory;
+ FoobarControlCenter* self = (FoobarControlCenter*)userdata;
+
+ GtkWidget* item = foobar_control_details_item_new( );
+ gtk_widget_set_margin_start( item, self->padding );
+ gtk_widget_set_margin_end( item, self->padding );
+ gtk_list_item_set_child( list_item, item );
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* label_expr = gtk_property_expression_new( FOOBAR_TYPE_BLUETOOTH_DEVICE, item_expr, "name" );
+ gtk_expression_bind( label_expr, item, "label", list_item );
+ }
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* state_expr = gtk_property_expression_new( FOOBAR_TYPE_BLUETOOTH_DEVICE, item_expr, "state" );
+ GtkExpression* accessory_params[] = { state_expr };
+ GtkExpression* accessory_expr = gtk_cclosure_expression_new(
+ FOOBAR_TYPE_CONTROL_DETAILS_ACCESSORY,
+ NULL,
+ G_N_ELEMENTS( accessory_params ),
+ accessory_params,
+ G_CALLBACK( foobar_control_center_compute_bluetooth_device_accessory ),
+ NULL,
+ NULL );
+ gtk_expression_bind( accessory_expr, item, "accessory", list_item );
+ }
+}
+
+//
+// Called by the bluetooth details list view when a device was selected.
+//
+void foobar_control_center_handle_bluetooth_device_activate(
+ GtkListView* view,
+ guint position,
+ gpointer userdata )
+{
+ (void)view;
+ (void)userdata;
+
+ GListModel* device_list = G_LIST_MODEL( gtk_list_view_get_model( view ) );
+ FoobarBluetoothDevice* device = g_list_model_get_item( device_list, position );
+ foobar_bluetooth_device_toggle_connection( device );
+}
+
+//
+// Called by the audio details list view to create a widget for displaying a device.
+//
+void foobar_control_center_handle_audio_device_setup(
+ GtkListItemFactory* factory,
+ GtkListItem* list_item,
+ gpointer userdata )
+{
+ (void)factory;
+ FoobarControlCenter* self = (FoobarControlCenter*)userdata;
+
+ GtkWidget* item = foobar_control_details_item_new( );
+ gtk_widget_set_margin_start( item, self->padding );
+ gtk_widget_set_margin_end( item, self->padding );
+ gtk_list_item_set_child( list_item, item );
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* label_expr = gtk_property_expression_new( FOOBAR_TYPE_AUDIO_DEVICE, item_expr, "description" );
+ gtk_expression_bind( label_expr, item, "label", list_item );
+ }
+
+ {
+ GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" );
+ GtkExpression* checked_expr = gtk_property_expression_new( FOOBAR_TYPE_AUDIO_DEVICE, item_expr, "is-default" );
+ gtk_expression_bind( checked_expr, item, "is-checked", list_item );
+ }
+}
+
+//
+// Called by the audio details list view when a device was selected.
+//
+void foobar_control_center_handle_audio_device_activate(
+ GtkListView* view,
+ guint position,
+ gpointer userdata )
+{
+ (void)view;
+ (void)userdata;
+
+ GListModel* device_list = G_LIST_MODEL( gtk_list_view_get_model( view ) );
+ FoobarAudioDevice* device = g_list_model_get_item( device_list, position );
+ foobar_audio_device_make_default( device );
+}
+
+//
+// Signal handler called when the global configuration file has changed.
+//
+void foobar_control_center_handle_config_change(
+ GObject* object,
+ GParamSpec* pspec,
+ gpointer userdata )
+{
+ (void)object;
+ (void)pspec;
+ FoobarControlCenter* self = (FoobarControlCenter*)userdata;
+
+ FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service );
+ foobar_control_center_apply_configuration(
+ self,
+ foobar_configuration_get_control_center( config ),
+ foobar_configuration_get_notifications( config ) );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Value Converters
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Derive the item accessory from a bluetooth device's connection state.
+//
+FoobarControlDetailsAccessory foobar_control_center_compute_bluetooth_device_accessory(
+ GtkExpression* expression,
+ FoobarBluetoothDeviceState state,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ switch ( state )
+ {
+ case FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED:
+ return FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE;
+ case FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING:
+ return FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS;
+ case FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED:
+ return FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED;
+ default:
+ g_warn_if_reached( );
+ return FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE;
+ }
+}
+
+//
+// Derive the visible child in the "notifications" stack from the number of notifications.
+//
+// If there are no notifications, a placeholder is shown.
+//
+gchar* foobar_control_center_compute_visible_notification_child_name(
+ GtkExpression* expression,
+ guint count,
+ gpointer userdata )
+{
+ (void)expression;
+ (void)userdata;
+
+ return count > 0 ? g_strdup( STACK_ITEM_LIST ) : g_strdup( STACK_ITEM_PLACEHOLDER );
+}