From 1af3d14f3f995645a0de08a54b244c19840709e9 Mon Sep 17 00:00:00 2001 From: Hannes Schulze Date: Thu, 19 Jun 2025 23:17:58 +0200 Subject: init hs project --- .gitignore | 1 + csrc/application.c | 691 +++++ csrc/application.h | 19 + csrc/control-center.c | 821 +++++ csrc/control-center.h | 27 + csrc/dbus/com.github.hannesschulze.foobar.xml | 8 + csrc/dbus/meson.build | 37 + csrc/dbus/org.bluez.xml | 66 + csrc/dbus/org.freedesktop.Notifications.xml | 35 + csrc/dbus/org.freedesktop.UPower.xml | 12 + csrc/launcher-item.c | 73 + csrc/launcher-item.h | 28 + csrc/launcher.c | 648 ++++ csrc/launcher.h | 20 + csrc/main.c | 12 + csrc/meson.build | 74 + csrc/notification-area.c | 196 ++ csrc/notification-area.h | 18 + csrc/panel.c | 348 +++ csrc/panel.h | 34 + csrc/services/application-service.c | 746 +++++ csrc/services/application-service.h | 25 + csrc/services/audio-service.c | 930 ++++++ csrc/services/audio-service.h | 44 + csrc/services/battery-service.c | 378 +++ csrc/services/battery-service.h | 36 + csrc/services/bluetooth-service.c | 949 ++++++ csrc/services/bluetooth-service.h | 41 + csrc/services/brightness-service.c | 391 +++ csrc/services/brightness-service.h | 16 + csrc/services/clock-service.c | 141 + csrc/services/clock-service.h | 14 + csrc/services/configuration-service.c | 3248 ++++++++++++++++++++ csrc/services/configuration-service.h | 305 ++ csrc/services/meson.build | 15 + csrc/services/network-service.c | 1577 ++++++++++ csrc/services/network-service.h | 62 + csrc/services/notification-service.c | 1833 +++++++++++ csrc/services/notification-service.h | 56 + csrc/services/quick-answer-service.c | 360 +++ csrc/services/quick-answer-service.h | 21 + csrc/services/quick-answers/math-lexer.c | 248 ++ csrc/services/quick-answers/math-parser.c | 414 +++ csrc/services/quick-answers/math.c | 851 +++++ csrc/services/quick-answers/math.h | 148 + csrc/services/quick-answers/math.test.c | 153 + csrc/services/quick-answers/meson.build | 9 + csrc/services/workspace-service.c | 1386 +++++++++ csrc/services/workspace-service.h | 42 + csrc/utils.c | 25 + csrc/utils.h | 9 + csrc/widgets/control-center/control-button.c | 496 +++ csrc/widgets/control-center/control-button.h | 31 + csrc/widgets/control-center/control-details-item.c | 307 ++ csrc/widgets/control-center/control-details-item.h | 32 + csrc/widgets/control-center/control-details.c | 289 ++ csrc/widgets/control-center/control-details.h | 22 + csrc/widgets/control-center/control-slider.c | 428 +++ csrc/widgets/control-center/control-slider.h | 28 + csrc/widgets/control-center/meson.build | 6 + csrc/widgets/inset-container.c | 363 +++ csrc/widgets/inset-container.h | 22 + csrc/widgets/limit-container.c | 341 ++ csrc/widgets/limit-container.h | 22 + csrc/widgets/meson.build | 8 + csrc/widgets/notification-widget.c | 821 +++++ csrc/widgets/notification-widget.h | 50 + csrc/widgets/panel/meson.build | 7 + csrc/widgets/panel/panel-item-clock.c | 152 + csrc/widgets/panel/panel-item-clock.h | 15 + csrc/widgets/panel/panel-item-icon.c | 92 + csrc/widgets/panel/panel-item-icon.h | 13 + csrc/widgets/panel/panel-item-status.c | 955 ++++++ csrc/widgets/panel/panel-item-status.h | 25 + csrc/widgets/panel/panel-item-workspaces.c | 327 ++ csrc/widgets/panel/panel-item-workspaces.h | 16 + csrc/widgets/panel/panel-item.c | 195 ++ csrc/widgets/panel/panel-item.h | 23 + flake.lock | 12 +- flake.nix | 8 + foobar.cabal | 15 + nix/package.nix | 3 +- src/Main.hs | 2 + src/application.c | 691 ----- src/application.h | 19 - src/control-center.c | 821 ----- src/control-center.h | 27 - src/dbus/com.github.hannesschulze.foobar.xml | 8 - src/dbus/meson.build | 37 - src/dbus/org.bluez.xml | 66 - src/dbus/org.freedesktop.Notifications.xml | 35 - src/dbus/org.freedesktop.UPower.xml | 12 - src/launcher-item.c | 73 - src/launcher-item.h | 28 - src/launcher.c | 648 ---- src/launcher.h | 20 - src/main.c | 12 - src/meson.build | 74 - src/notification-area.c | 196 -- src/notification-area.h | 18 - src/panel.c | 348 --- src/panel.h | 34 - src/services/application-service.c | 746 ----- src/services/application-service.h | 25 - src/services/audio-service.c | 930 ------ src/services/audio-service.h | 44 - src/services/battery-service.c | 378 --- src/services/battery-service.h | 36 - src/services/bluetooth-service.c | 949 ------ src/services/bluetooth-service.h | 41 - src/services/brightness-service.c | 391 --- src/services/brightness-service.h | 16 - src/services/clock-service.c | 141 - src/services/clock-service.h | 14 - src/services/configuration-service.c | 3248 -------------------- src/services/configuration-service.h | 305 -- src/services/meson.build | 15 - src/services/network-service.c | 1577 ---------- src/services/network-service.h | 62 - src/services/notification-service.c | 1833 ----------- src/services/notification-service.h | 56 - src/services/quick-answer-service.c | 360 --- src/services/quick-answer-service.h | 21 - src/services/quick-answers/math-lexer.c | 248 -- src/services/quick-answers/math-parser.c | 414 --- src/services/quick-answers/math.c | 851 ----- src/services/quick-answers/math.h | 148 - src/services/quick-answers/math.test.c | 153 - src/services/quick-answers/meson.build | 9 - src/services/workspace-service.c | 1386 --------- src/services/workspace-service.h | 42 - src/utils.c | 25 - src/utils.h | 9 - src/widgets/control-center/control-button.c | 496 --- src/widgets/control-center/control-button.h | 31 - src/widgets/control-center/control-details-item.c | 307 -- src/widgets/control-center/control-details-item.h | 32 - src/widgets/control-center/control-details.c | 289 -- src/widgets/control-center/control-details.h | 22 - src/widgets/control-center/control-slider.c | 428 --- src/widgets/control-center/control-slider.h | 28 - src/widgets/control-center/meson.build | 6 - src/widgets/inset-container.c | 363 --- src/widgets/inset-container.h | 22 - src/widgets/limit-container.c | 341 -- src/widgets/limit-container.h | 22 - src/widgets/meson.build | 8 - src/widgets/notification-widget.c | 821 ----- src/widgets/notification-widget.h | 50 - 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 - subprojects/gtk4-layer-shell.wrap | 2 +- 161 files changed, 22761 insertions(+), 22734 deletions(-) create mode 100644 csrc/application.c create mode 100644 csrc/application.h create mode 100644 csrc/control-center.c create mode 100644 csrc/control-center.h create mode 100644 csrc/dbus/com.github.hannesschulze.foobar.xml create mode 100644 csrc/dbus/meson.build create mode 100644 csrc/dbus/org.bluez.xml create mode 100644 csrc/dbus/org.freedesktop.Notifications.xml create mode 100644 csrc/dbus/org.freedesktop.UPower.xml create mode 100644 csrc/launcher-item.c create mode 100644 csrc/launcher-item.h create mode 100644 csrc/launcher.c create mode 100644 csrc/launcher.h create mode 100644 csrc/main.c create mode 100644 csrc/meson.build create mode 100644 csrc/notification-area.c create mode 100644 csrc/notification-area.h create mode 100644 csrc/panel.c create mode 100644 csrc/panel.h create mode 100644 csrc/services/application-service.c create mode 100644 csrc/services/application-service.h create mode 100644 csrc/services/audio-service.c create mode 100644 csrc/services/audio-service.h create mode 100644 csrc/services/battery-service.c create mode 100644 csrc/services/battery-service.h create mode 100644 csrc/services/bluetooth-service.c create mode 100644 csrc/services/bluetooth-service.h create mode 100644 csrc/services/brightness-service.c create mode 100644 csrc/services/brightness-service.h create mode 100644 csrc/services/clock-service.c create mode 100644 csrc/services/clock-service.h create mode 100644 csrc/services/configuration-service.c create mode 100644 csrc/services/configuration-service.h create mode 100644 csrc/services/meson.build create mode 100644 csrc/services/network-service.c create mode 100644 csrc/services/network-service.h create mode 100644 csrc/services/notification-service.c create mode 100644 csrc/services/notification-service.h create mode 100644 csrc/services/quick-answer-service.c create mode 100644 csrc/services/quick-answer-service.h create mode 100644 csrc/services/quick-answers/math-lexer.c create mode 100644 csrc/services/quick-answers/math-parser.c create mode 100644 csrc/services/quick-answers/math.c create mode 100644 csrc/services/quick-answers/math.h create mode 100644 csrc/services/quick-answers/math.test.c create mode 100644 csrc/services/quick-answers/meson.build create mode 100644 csrc/services/workspace-service.c create mode 100644 csrc/services/workspace-service.h create mode 100644 csrc/utils.c create mode 100644 csrc/utils.h create mode 100644 csrc/widgets/control-center/control-button.c create mode 100644 csrc/widgets/control-center/control-button.h create mode 100644 csrc/widgets/control-center/control-details-item.c create mode 100644 csrc/widgets/control-center/control-details-item.h create mode 100644 csrc/widgets/control-center/control-details.c create mode 100644 csrc/widgets/control-center/control-details.h create mode 100644 csrc/widgets/control-center/control-slider.c create mode 100644 csrc/widgets/control-center/control-slider.h create mode 100644 csrc/widgets/control-center/meson.build create mode 100644 csrc/widgets/inset-container.c create mode 100644 csrc/widgets/inset-container.h create mode 100644 csrc/widgets/limit-container.c create mode 100644 csrc/widgets/limit-container.h create mode 100644 csrc/widgets/meson.build create mode 100644 csrc/widgets/notification-widget.c create mode 100644 csrc/widgets/notification-widget.h create mode 100644 csrc/widgets/panel/meson.build create mode 100644 csrc/widgets/panel/panel-item-clock.c create mode 100644 csrc/widgets/panel/panel-item-clock.h create mode 100644 csrc/widgets/panel/panel-item-icon.c create mode 100644 csrc/widgets/panel/panel-item-icon.h create mode 100644 csrc/widgets/panel/panel-item-status.c create mode 100644 csrc/widgets/panel/panel-item-status.h create mode 100644 csrc/widgets/panel/panel-item-workspaces.c create mode 100644 csrc/widgets/panel/panel-item-workspaces.h create mode 100644 csrc/widgets/panel/panel-item.c create mode 100644 csrc/widgets/panel/panel-item.h create mode 100644 foobar.cabal create mode 100644 src/Main.hs delete mode 100644 src/application.c delete mode 100644 src/application.h delete mode 100644 src/control-center.c delete mode 100644 src/control-center.h delete mode 100644 src/dbus/com.github.hannesschulze.foobar.xml delete mode 100644 src/dbus/meson.build delete mode 100644 src/dbus/org.bluez.xml delete mode 100644 src/dbus/org.freedesktop.Notifications.xml delete mode 100644 src/dbus/org.freedesktop.UPower.xml delete mode 100644 src/launcher-item.c delete mode 100644 src/launcher-item.h delete mode 100644 src/launcher.c delete mode 100644 src/launcher.h delete mode 100644 src/main.c delete mode 100644 src/meson.build delete mode 100644 src/notification-area.c delete mode 100644 src/notification-area.h delete mode 100644 src/panel.c delete mode 100644 src/panel.h delete mode 100644 src/services/application-service.c delete mode 100644 src/services/application-service.h delete mode 100644 src/services/audio-service.c delete mode 100644 src/services/audio-service.h delete mode 100644 src/services/battery-service.c delete mode 100644 src/services/battery-service.h delete mode 100644 src/services/bluetooth-service.c delete mode 100644 src/services/bluetooth-service.h delete mode 100644 src/services/brightness-service.c delete mode 100644 src/services/brightness-service.h delete mode 100644 src/services/clock-service.c delete mode 100644 src/services/clock-service.h delete mode 100644 src/services/configuration-service.c delete mode 100644 src/services/configuration-service.h delete mode 100644 src/services/meson.build delete mode 100644 src/services/network-service.c delete mode 100644 src/services/network-service.h delete mode 100644 src/services/notification-service.c delete mode 100644 src/services/notification-service.h delete mode 100644 src/services/quick-answer-service.c delete mode 100644 src/services/quick-answer-service.h delete mode 100644 src/services/quick-answers/math-lexer.c delete mode 100644 src/services/quick-answers/math-parser.c delete mode 100644 src/services/quick-answers/math.c delete mode 100644 src/services/quick-answers/math.h delete mode 100644 src/services/quick-answers/math.test.c delete mode 100644 src/services/quick-answers/meson.build delete mode 100644 src/services/workspace-service.c delete mode 100644 src/services/workspace-service.h delete mode 100644 src/utils.c delete mode 100644 src/utils.h delete mode 100644 src/widgets/control-center/control-button.c delete mode 100644 src/widgets/control-center/control-button.h delete mode 100644 src/widgets/control-center/control-details-item.c delete mode 100644 src/widgets/control-center/control-details-item.h delete mode 100644 src/widgets/control-center/control-details.c delete mode 100644 src/widgets/control-center/control-details.h delete mode 100644 src/widgets/control-center/control-slider.c delete mode 100644 src/widgets/control-center/control-slider.h delete mode 100644 src/widgets/control-center/meson.build delete mode 100644 src/widgets/inset-container.c delete mode 100644 src/widgets/inset-container.h delete mode 100644 src/widgets/limit-container.c delete mode 100644 src/widgets/limit-container.h delete mode 100644 src/widgets/meson.build delete mode 100644 src/widgets/notification-widget.c delete mode 100644 src/widgets/notification-widget.h delete mode 100644 src/widgets/panel/meson.build delete mode 100644 src/widgets/panel/panel-item-clock.c delete mode 100644 src/widgets/panel/panel-item-clock.h delete mode 100644 src/widgets/panel/panel-item-icon.c delete mode 100644 src/widgets/panel/panel-item-icon.h delete mode 100644 src/widgets/panel/panel-item-status.c delete mode 100644 src/widgets/panel/panel-item-status.h delete mode 100644 src/widgets/panel/panel-item-workspaces.c delete mode 100644 src/widgets/panel/panel-item-workspaces.h delete mode 100644 src/widgets/panel/panel-item.c delete mode 100644 src/widgets/panel/panel-item.h diff --git a/.gitignore b/.gitignore index ac5be07..b83620d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build/ .vscode/ .cache/ subprojects/*/ +dist*/ diff --git a/csrc/application.c b/csrc/application.c new file mode 100644 index 0000000..bad62b1 --- /dev/null +++ b/csrc/application.c @@ -0,0 +1,691 @@ +#include "application.h" +#include "panel.h" +#include "launcher.h" +#include "control-center.h" +#include "notification-area.h" +#include "dbus/server.h" +#include "services/battery-service.h" +#include "services/clock-service.h" +#include "services/brightness-service.h" +#include "services/quick-answer-service.h" +#include "services/workspace-service.h" +#include "services/notification-service.h" +#include "services/audio-service.h" +#include "services/network-service.h" +#include "services/bluetooth-service.h" +#include "services/application-service.h" +#include "services/configuration-service.h" + +// +// FoobarApplication: +// +// Entry point for the application. This class is responsible for parsing and handling command line arguments, creating +// services and presenting windows as needed. +// +// Only the panel windows are application windows -- when all panels are closed, the application quits. Because of this, +// the application can't own a reference to the panel windows but needs to store their window IDs. Since all other +// windows are not attached to the application object, the application can hold a reference on them. +// + +struct _FoobarApplication +{ + GtkApplication parent_instance; + FoobarBatteryService* battery_service; + FoobarClockService* clock_service; + FoobarBrightnessService* brightness_service; + FoobarWorkspaceService* workspace_service; + FoobarNotificationService* notification_service; + FoobarAudioService* audio_service; + FoobarNetworkService* network_service; + FoobarBluetoothService* bluetooth_service; + FoobarApplicationService* application_service; + FoobarQuickAnswerService* quick_answer_service; + FoobarConfigurationService* configuration_service; + gulong config_handler_id; + FoobarServer* server_skeleton; + GtkCssProvider* style_provider; + guint bus_owner_id; + GArray* panel_window_ids; + gboolean panel_is_multi_monitor; + GListModel* monitors; + gulong monitors_handler_id; + FoobarLauncher* launcher; + FoobarControlCenter* control_center; + FoobarNotificationArea* notification_area; + gboolean option_inspector; + gboolean option_quit; + gboolean option_toggle_launcher; + gboolean option_toggle_control_center; +}; + +static void foobar_application_class_init ( FoobarApplicationClass* klass ); +static void foobar_application_init ( FoobarApplication* self ); +static void foobar_application_activate ( GApplication* app ); +static int foobar_application_command_line ( GApplication* app, + GApplicationCommandLine* cmdline ); +static void foobar_application_finalize ( GObject* object ); +static void foobar_application_handle_bus_acquired ( GDBusConnection* connection, + gchar const* name, + gpointer userdata ); +static gboolean foobar_application_handle_inspector ( FoobarServer* server, + GDBusMethodInvocation* invocation, + gpointer userdata ); +static gboolean foobar_application_handle_quit ( FoobarServer* server, + GDBusMethodInvocation* invocation, + gpointer userdata ); +static gboolean foobar_application_handle_toggle_launcher ( FoobarServer* server, + GDBusMethodInvocation* invocation, + gpointer userdata ); +static gboolean foobar_application_handle_toggle_control_center( FoobarServer* server, + GDBusMethodInvocation* invocation, + gpointer userdata ); +static void foobar_application_handle_config_changed ( GObject* object, + GParamSpec* pspec, + gpointer userdata ); +static void foobar_application_handle_monitors_changed ( GListModel* list, + guint position, + guint removed, + guint added, + gpointer userdata ); +static void foobar_application_destroy_panels ( FoobarApplication* self ); +static void foobar_application_create_panels ( FoobarApplication* self ); +static guint foobar_application_create_panel ( FoobarApplication* self, + GdkMonitor* monitor ); + +G_DEFINE_FINAL_TYPE( FoobarApplication, foobar_application, GTK_TYPE_APPLICATION ) + +// --------------------------------------------------------------------------------------------------------------------- +// Application Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the application. +// +void foobar_application_class_init( FoobarApplicationClass* klass ) +{ + GApplicationClass* app_klass = G_APPLICATION_CLASS( klass ); + app_klass->activate = foobar_application_activate; + app_klass->command_line = foobar_application_command_line; + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->finalize = foobar_application_finalize; +} + +// +// Instance initialization for the application. +// +void foobar_application_init( FoobarApplication* self ) +{ + g_application_set_default( G_APPLICATION( self ) ); + g_application_set_option_context_summary( G_APPLICATION( self ), "A bar." ); + + GOptionEntry const options[] = + { + { + .long_name = "inspector", + .short_name = 'i', + .flags = G_OPTION_FLAG_NONE, + .arg = G_OPTION_ARG_NONE, + .arg_data = &self->option_inspector, + .description = "Open the GTK inspector for the active bar instance.", + .arg_description = NULL, + }, + { + .long_name = "quit", + .short_name = 'q', + .flags = G_OPTION_FLAG_NONE, + .arg = G_OPTION_ARG_NONE, + .arg_data = &self->option_quit, + .description = "Quit the active bar instance.", + .arg_description = NULL, + }, + { + .long_name = "toggle-launcher", + .short_name = 'l', + .flags = G_OPTION_FLAG_NONE, + .arg = G_OPTION_ARG_NONE, + .arg_data = &self->option_toggle_launcher, + .description = "Toggle the launcher visibility for the active bar instance.", + .arg_description = NULL, + }, + { + .long_name = "toggle-control-center", + .short_name = 'c', + .flags = G_OPTION_FLAG_NONE, + .arg = G_OPTION_ARG_NONE, + .arg_data = &self->option_toggle_control_center, + .description = "Toggle the control center visibility for the active bar instance.", + .arg_description = NULL, + }, + { 0 }, + }; + g_application_add_main_option_entries( G_APPLICATION( self ), options ); +} + +// +// Called by GTK when the application is activated (only for a single instance of the application). +// +// This is where the services are created and the windows are presented. +// +void foobar_application_activate( GApplication* app ) +{ + FoobarApplication* self = (FoobarApplication*)app; + + // Initialize services. + + self->battery_service = foobar_battery_service_new( ); + self->clock_service = foobar_clock_service_new( ); + self->brightness_service = foobar_brightness_service_new( ); + self->workspace_service = foobar_workspace_service_new( ); + self->notification_service = foobar_notification_service_new( ); + self->audio_service = foobar_audio_service_new( ); + self->network_service = foobar_network_service_new( ); + self->bluetooth_service = foobar_bluetooth_service_new( ); + self->application_service = foobar_application_service_new( ); + self->quick_answer_service = foobar_quick_answer_service_new( ); + self->configuration_service = foobar_configuration_service_new( ); + + // Enforce a uniform style by forcing Adwaita and shipping our own icons. + + GtkSettings* settings = gtk_settings_get_default( ); + g_object_set( settings, "gtk-theme-name", "Adwaita", NULL ); + + GtkIconTheme* icon_theme = gtk_icon_theme_get_for_display( gdk_display_get_default( ) ); + gtk_icon_theme_add_resource_path( icon_theme, "/foobar/icons" ); + + // This application instance now becomes the "server" process, listening for commands from clients (which can be + // invoked via the command line). + + self->bus_owner_id = g_bus_own_name( + G_BUS_TYPE_SESSION, + g_application_get_application_id( G_APPLICATION( self ) ), + G_BUS_NAME_OWNER_FLAGS_NONE, + foobar_application_handle_bus_acquired, + NULL, + NULL, + self, + NULL ); + + // React to configuration file changes. + + FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); + foobar_application_apply_configuration( self, foobar_configuration_get_general( config ) ); + self->panel_is_multi_monitor = foobar_panel_configuration_get_multi_monitor( foobar_configuration_get_panel( config ) ); + self->config_handler_id = g_signal_connect( + self->configuration_service, + "notify::current", + G_CALLBACK( foobar_application_handle_config_changed ), + self ); + + // React to monitor configuration changes (for multi-monitor support in the panel). + + self->monitors = gdk_display_get_monitors( gdk_display_get_default( ) ); + g_object_ref( self->monitors ); + + self->monitors_handler_id = g_signal_connect( + self->monitors, + "items-changed", + G_CALLBACK( foobar_application_handle_monitors_changed ), + self ); + + // Create all the windows. + + foobar_application_create_panels( self ); + + self->launcher = foobar_launcher_new( + self->application_service, + self->quick_answer_service, + self->configuration_service ); + g_object_ref( self->launcher ); + + self->control_center = foobar_control_center_new( + self->brightness_service, + self->audio_service, + self->network_service, + self->bluetooth_service, + self->notification_service, + self->configuration_service ); + g_object_ref( self->control_center ); + + self->notification_area = foobar_notification_area_new( + self->notification_service, + self->configuration_service ); + g_object_ref( self->notification_area ); + + // Only present the notification area, all other windows start out as hidden windows. + + gtk_window_present( GTK_WINDOW( self->notification_area ) ); +} + +// +// Called before activating the application, when GTK has parsed the command line arguments. +// +// This method decides whether the application should be activated (if it is the first instance). Otherwise it will +// handle command line flags for controlling the application. +// +int foobar_application_command_line( + GApplication* app, + GApplicationCommandLine* cmdline ) +{ + (void)cmdline; + FoobarApplication* self = (FoobarApplication*)app; + + // Try to open a connection to the existing server. + + g_autoptr( GError ) error = NULL; + g_autoptr( FoobarServer ) proxy = foobar_server_proxy_new_for_bus_sync( + G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + g_application_get_application_id( G_APPLICATION( self ) ), + "/com/github/hannesschulze/foobar/server", + NULL, + &error ); + + if ( !proxy ) + { + g_printerr( "Unable to create proxy: %s\n", error->message ); + return 1; + } + + // If there is no existing server application, the "name owner" will not be set. + + g_autofree gchar* owner = g_dbus_proxy_get_name_owner( G_DBUS_PROXY( proxy ) ); + if ( owner ) + { + // There is an active instance -> this is a client. Handle all command line flags. + + if ( self->option_inspector && !foobar_server_call_inspector_sync( proxy, NULL, &error ) ) + { + g_printerr( "Unable to open inspector: %s\n", error->message ); + return 2; + } + + if ( self->option_toggle_launcher && !foobar_server_call_toggle_launcher_sync( proxy, NULL, &error ) ) + { + g_printerr( "Unable to toggle launcher: %s\n", error->message ); + return 2; + } + + if ( self->option_toggle_control_center && !foobar_server_call_toggle_control_center_sync( proxy, NULL, &error ) ) + { + g_printerr( "Unable to toggle control center: %s\n", error->message ); + return 2; + } + + if ( self->option_quit && !foobar_server_call_quit_sync( proxy, NULL, &error ) ) + { + g_printerr( "Unable to quit server: %s\n", error->message ); + return 2; + } + } + else if ( !self->option_quit ) + { + // This is the main application -> launch the server. + + g_application_activate( G_APPLICATION( self ) ); + } + + return 0; +} + +// +// Instance cleanup for the application. +// +void foobar_application_finalize( GObject* object ) +{ + FoobarApplication* self = (FoobarApplication*)object; + + // Destroy the DBus server. + + if ( self->server_skeleton ) + { + g_dbus_interface_skeleton_unexport( G_DBUS_INTERFACE_SKELETON( self->server_skeleton ) ); + } + + // Destroy non-application windows. + + if ( self->launcher ) { gtk_window_destroy( GTK_WINDOW( self->launcher ) ); } + if ( self->control_center ) { gtk_window_destroy( GTK_WINDOW( self->control_center ) ); } + if ( self->notification_area ) { gtk_window_destroy( GTK_WINDOW( self->notification_area ) ); } + + // Destroy all other objects. + + g_clear_handle_id( &self->bus_owner_id, g_bus_unown_name ); + g_clear_signal_handler( &self->config_handler_id, self->configuration_service ); + g_clear_signal_handler( &self->monitors_handler_id, self->monitors ); + g_clear_object( &self->battery_service ); + g_clear_object( &self->clock_service ); + g_clear_object( &self->brightness_service ); + g_clear_object( &self->workspace_service ); + g_clear_object( &self->notification_service ); + g_clear_object( &self->audio_service ); + g_clear_object( &self->network_service ); + g_clear_object( &self->bluetooth_service ); + g_clear_object( &self->application_service ); + g_clear_object( &self->quick_answer_service ); + g_clear_object( &self->configuration_service ); + g_clear_object( &self->style_provider ); + g_clear_object( &self->server_skeleton ); + g_clear_object( &self->launcher ); + g_clear_object( &self->control_center ); + g_clear_object( &self->notification_area ); + g_clear_object( &self->monitors ); + g_clear_pointer( &self->panel_window_ids, g_array_unref ); + + G_OBJECT_CLASS( foobar_application_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new application instance. +// +FoobarApplication* foobar_application_new( void ) +{ + return g_object_new( + FOOBAR_TYPE_APPLICATION, + "application-id", + "com.github.hannesschulze.foobar", + "flags", + G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_NON_UNIQUE, + NULL ); +} + +// +// Toggle the launcher window's visibility. +// +void foobar_application_toggle_launcher( FoobarApplication* self ) +{ + g_return_if_fail( FOOBAR_IS_APPLICATION( self ) ); + + gboolean previously_visible = gtk_widget_is_visible( GTK_WIDGET( self->launcher ) ); + gtk_widget_set_visible( GTK_WIDGET( self->launcher ), !previously_visible ); +} + +// +// Toggle the control center window's visibility. +// +void foobar_application_toggle_control_center( FoobarApplication* self ) +{ + g_return_if_fail( FOOBAR_IS_APPLICATION( self ) ); + + gboolean previously_visible = gtk_widget_is_visible( GTK_WIDGET( self->control_center ) ); + gtk_widget_set_visible( GTK_WIDGET( self->control_center ), !previously_visible ); +} + +// +// Apply the general application configuration provided by the configuration service. +// +void foobar_application_apply_configuration( + FoobarApplication* self, + FoobarGeneralConfiguration const* config ) +{ + g_return_if_fail( FOOBAR_IS_APPLICATION( self ) ); + g_return_if_fail( config != NULL ); + + GdkDisplay* display = gdk_display_get_default( ); + + // Remove the old stylesheet from the application. + + if ( self->style_provider ) + { + gtk_style_context_remove_provider_for_display( display, GTK_STYLE_PROVIDER( self->style_provider ) ); + g_clear_object( &self->style_provider ); + } + + // Add the new stylesheet. + + g_autoptr( GFile ) file = g_file_new_for_uri( foobar_general_configuration_get_stylesheet( config ) ); + self->style_provider = gtk_css_provider_new( ); + gtk_css_provider_load_from_file( self->style_provider, file ); + gtk_style_context_add_provider_for_display( + display, + GTK_STYLE_PROVIDER( self->style_provider ), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called after the application server process has asynchronously acquired the bus. +// +// This is where we can export server functionality using the corresponding DBus skeleton object. +// +void foobar_application_handle_bus_acquired( + GDBusConnection* connection, + gchar const* name, + gpointer userdata ) +{ + (void)name; + FoobarApplication* self = (FoobarApplication*)userdata; + + self->server_skeleton = foobar_server_skeleton_new( ); + g_signal_connect( + self->server_skeleton, + "handle-inspector", + G_CALLBACK( foobar_application_handle_inspector ), + self ); + g_signal_connect( + self->server_skeleton, + "handle-quit", + G_CALLBACK( foobar_application_handle_quit ), + self ); + g_signal_connect( + self->server_skeleton, + "handle-toggle-launcher", + G_CALLBACK( foobar_application_handle_toggle_launcher ), + self ); + g_signal_connect( + self->server_skeleton, + "handle-toggle-control-center", + G_CALLBACK( foobar_application_handle_toggle_control_center ), + self ); + + g_autoptr( GError ) error = NULL; + if ( !g_dbus_interface_skeleton_export( + G_DBUS_INTERFACE_SKELETON( self->server_skeleton ), + connection, + "/com/github/hannesschulze/foobar/server", + &error ) ) + { + g_warning( "Unable to export server interface: %s", error->message ); + } +} + +// +// DBus skeleton callback for the "Inspector" method. +// +gboolean foobar_application_handle_inspector( + FoobarServer* server, + GDBusMethodInvocation* invocation, + gpointer userdata ) +{ + (void)userdata; + + gtk_window_set_interactive_debugging( TRUE ); + + foobar_server_complete_inspector( server, invocation ); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +// +// DBus skeleton callback for the "Quit" method. +// +gboolean foobar_application_handle_quit( + FoobarServer* server, + GDBusMethodInvocation* invocation, + gpointer userdata ) +{ + FoobarApplication* self = (FoobarApplication*)userdata; + + foobar_application_destroy_panels( self ); + + foobar_server_complete_quit( server, invocation ); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +// +// DBus skeleton callback for the "ToggleLauncher" method. +// +gboolean foobar_application_handle_toggle_launcher( + FoobarServer* server, + GDBusMethodInvocation* invocation, + gpointer userdata ) +{ + FoobarApplication* self = (FoobarApplication*)userdata; + + foobar_application_toggle_launcher( self ); + + foobar_server_complete_toggle_launcher( server, invocation ); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +// +// DBus skeleton callback for the "ToggleControlCenter" method. +// +gboolean foobar_application_handle_toggle_control_center( + FoobarServer* server, + GDBusMethodInvocation* invocation, + gpointer userdata ) +{ + FoobarApplication* self = (FoobarApplication*)userdata; + + foobar_application_toggle_control_center( self ); + + foobar_server_complete_toggle_control_center( server, invocation ); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +// +// Signal handler called when the global configuration file has changed. +// +void foobar_application_handle_config_changed( + GObject* object, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)object; + (void)pspec; + FoobarApplication* self = (FoobarApplication*)userdata; + + // Apply general settings. + + FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); + foobar_application_apply_configuration( self, foobar_configuration_get_general( config ) ); + + // Update panel instances depending on whether multi-monitor mode is enabled. + + FoobarPanelConfiguration const* panel_config = foobar_configuration_get_panel( config ); + gboolean panel_is_multi_monitor = foobar_panel_configuration_get_multi_monitor( panel_config ); + if ( self->panel_is_multi_monitor != panel_is_multi_monitor ) + { + self->panel_is_multi_monitor = panel_is_multi_monitor; + + // Panel windows keep the application alive, so we need to "hold" it while we re-create the panel windows. + + g_application_hold( G_APPLICATION( self ) ); + foobar_application_destroy_panels( self ); + foobar_application_create_panels( self ); + g_application_release( G_APPLICATION( self ) ); + } +} + +// +// Signal handler called when the list of GdkMonitor objects has changed, possibly requiring us to re-create the panel +// windows. +// +void foobar_application_handle_monitors_changed( + GListModel* list, + guint position, + guint removed, + guint added, + gpointer userdata ) +{ + (void)list; + (void)position; + (void)removed; + (void)added; + FoobarApplication* self = (FoobarApplication*)userdata; + + if ( self->panel_is_multi_monitor ) + { + // Panel windows keep the application alive, so we need to "hold" it while we re-create the panel windows. + + g_application_hold( G_APPLICATION( self ) ); + foobar_application_destroy_panels( self ); + foobar_application_create_panels( self ); + g_application_release( G_APPLICATION( self ) ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Destroy all currently active panel windows. +// +void foobar_application_destroy_panels( FoobarApplication* self ) +{ + for ( guint i = 0; i < self->panel_window_ids->len; ++i ) + { + guint window_id = g_array_index( self->panel_window_ids, guint, i ); + GtkWindow* window = gtk_application_get_window_by_id( GTK_APPLICATION( self ), window_id ); + if ( window ) { gtk_window_destroy( window ); } + } + + g_clear_pointer( &self->panel_window_ids, g_array_unref ); +} + +// +// Create and present panel windows for +// - each monitor, if multi-monitor mode is enabled +// - the current monitor, if multi-monitor mode is disabled. +// +void foobar_application_create_panels( FoobarApplication* self ) +{ + self->panel_window_ids = g_array_new( FALSE, FALSE, sizeof( guint ) ); + + if ( self->panel_is_multi_monitor ) + { + for ( guint i = 0; i < g_list_model_get_n_items( self->monitors ); ++i ) + { + GdkMonitor* monitor = g_list_model_get_item( self->monitors, i ); + guint id = foobar_application_create_panel( self, monitor ); + g_array_append_val( self->panel_window_ids, id ); + } + } + else + { + guint id = foobar_application_create_panel( self, NULL ); + g_array_append_val( self->panel_window_ids, id ); + } +} + +// +// Create and present a panel for a single monitor (or NULL to use the current monitor). +// +guint foobar_application_create_panel( + FoobarApplication* self, + GdkMonitor* monitor ) +{ + FoobarPanel* panel = foobar_panel_new( + GTK_APPLICATION( self ), + monitor, + self->battery_service, + self->clock_service, + self->brightness_service, + self->workspace_service, + self->audio_service, + self->network_service, + self->bluetooth_service, + self->notification_service, + self->configuration_service ); + gtk_window_present( GTK_WINDOW( panel ) ); + + return gtk_application_window_get_id( GTK_APPLICATION_WINDOW( panel ) ); +} diff --git a/csrc/application.h b/csrc/application.h new file mode 100644 index 0000000..c5353c3 --- /dev/null +++ b/csrc/application.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include "services/configuration-service.h" + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_APPLICATION foobar_application_get_type( ) +#define FOOBAR_APPLICATION_DEFAULT FOOBAR_APPLICATION( g_application_get_default( ) ) + +G_DECLARE_FINAL_TYPE( FoobarApplication, foobar_application, FOOBAR, APPLICATION, GtkApplication ) + +FoobarApplication* foobar_application_new ( void ); +void foobar_application_toggle_launcher ( FoobarApplication* self ); +void foobar_application_toggle_control_center( FoobarApplication* self ); +void foobar_application_apply_configuration ( FoobarApplication* self, + FoobarGeneralConfiguration const* config ); + +G_END_DECLS 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 + +#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 ); +} diff --git a/csrc/control-center.h b/csrc/control-center.h new file mode 100644 index 0000000..c320017 --- /dev/null +++ b/csrc/control-center.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#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" +#include "services/configuration-service.h" + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_CONTROL_CENTER foobar_control_center_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarControlCenter, foobar_control_center, FOOBAR, CONTROL_CENTER, GtkWindow ) + +FoobarControlCenter* foobar_control_center_new ( FoobarBrightnessService* brightness_service, + FoobarAudioService* audio_service, + FoobarNetworkService* network_service, + FoobarBluetoothService* bluetooth_service, + FoobarNotificationService* notification_service, + FoobarConfigurationService* configuration_service ); +void foobar_control_center_apply_configuration( FoobarControlCenter* self, + FoobarControlCenterConfiguration const* config, + FoobarNotificationConfiguration const* notification_config ); + +G_END_DECLS diff --git a/csrc/dbus/com.github.hannesschulze.foobar.xml b/csrc/dbus/com.github.hannesschulze.foobar.xml new file mode 100644 index 0000000..e8373a6 --- /dev/null +++ b/csrc/dbus/com.github.hannesschulze.foobar.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/csrc/dbus/meson.build b/csrc/dbus/meson.build new file mode 100644 index 0000000..80062bd --- /dev/null +++ b/csrc/dbus/meson.build @@ -0,0 +1,37 @@ +foobar_sources += gnome.gdbus_codegen( + 'server', + 'com.github.hannesschulze.foobar.xml', + namespace: 'Foobar', + annotations: [ + ['com.github.hannesschulze.foobar.Server', 'org.gtk.GDBus.C.Name', 'Server'], + ], +) + +foobar_sources += gnome.gdbus_codegen( + 'notifications', + 'org.freedesktop.Notifications.xml', + namespace: 'Foobar', + annotations: [ + ['org.freedesktop.Notifications', 'org.gtk.GDBus.C.Name', 'Notifications'], + ], +) + +foobar_sources += gnome.gdbus_codegen( + 'upower', + 'org.freedesktop.UPower.xml', + namespace: 'Foobar', + annotations: [ + ['org.freedesktop.UPower.Device', 'org.gtk.GDBus.C.Name', 'UPowerDevice'], + ], +) + +foobar_sources += gnome.gdbus_codegen( + 'bluez', + 'org.bluez.xml', + namespace: 'Foobar', + annotations: [ + ['org.bluez.Adapter1', 'org.gtk.GDBus.C.Name', 'BluezAdapter'], + ['org.bluez.Device1', 'org.gtk.GDBus.C.Name', 'BluezDevice'], + ['org.bluez.AgentManager1', 'org.gtk.GDBus.C.Name', 'BluezAgentManager'], + ], +) diff --git a/csrc/dbus/org.bluez.xml b/csrc/dbus/org.bluez.xml new file mode 100644 index 0000000..669e325 --- /dev/null +++ b/csrc/dbus/org.bluez.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/csrc/dbus/org.freedesktop.Notifications.xml b/csrc/dbus/org.freedesktop.Notifications.xml new file mode 100644 index 0000000..6b1d2f2 --- /dev/null +++ b/csrc/dbus/org.freedesktop.Notifications.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/csrc/dbus/org.freedesktop.UPower.xml b/csrc/dbus/org.freedesktop.UPower.xml new file mode 100644 index 0000000..18fc41d --- /dev/null +++ b/csrc/dbus/org.freedesktop.UPower.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/csrc/launcher-item.c b/csrc/launcher-item.c new file mode 100644 index 0000000..c4de70b --- /dev/null +++ b/csrc/launcher-item.c @@ -0,0 +1,73 @@ +#include "launcher-item.h" + +static void foobar_launcher_item_default_init( FoobarLauncherItemInterface* iface ); + +G_DEFINE_INTERFACE( FoobarLauncherItem, foobar_launcher_item, G_TYPE_OBJECT ) + +// +// Static initialization for the launcher item interface. +// +void foobar_launcher_item_default_init( FoobarLauncherItemInterface* iface ) +{ + g_object_interface_install_property( iface, g_param_spec_string( + "title", + "Title", + "The title text displayed in the launcher.", + NULL, + G_PARAM_READABLE ) ); + g_object_interface_install_property( iface, g_param_spec_string( + "description", + "Description", + "Text displayed below the title.", + NULL, + G_PARAM_READABLE ) ); + g_object_interface_install_property( iface, g_param_spec_object( + "icon", + "Icon", + "Icon to show for the item.", + G_TYPE_ICON, + G_PARAM_READABLE ) ); +} + +// +// Get the title text displayed in the launcher. +// +gchar const* foobar_launcher_item_get_title( FoobarLauncherItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ), NULL ); + FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self ); + g_return_val_if_fail( iface->get_title != NULL, NULL ); + return iface->get_title( self ); +} + +// +// Get the text displayed below the title. +// +gchar const* foobar_launcher_item_get_description( FoobarLauncherItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ), NULL ); + FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self ); + g_return_val_if_fail( iface->get_description != NULL, NULL ); + return iface->get_description( self ); +} + +// +// Get the icon to show for the item. +// +GIcon* foobar_launcher_item_get_icon( FoobarLauncherItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ), NULL ); + FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self ); + g_return_val_if_fail( iface->get_icon != NULL, NULL ); + return iface->get_icon( self ); +} + +// +// Activate the item, e.g. by launching the application. +// +void foobar_launcher_item_activate( FoobarLauncherItem* self ) +{ + g_return_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ) ); + FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self ); + if ( iface->activate ) { iface->activate( self ); } +} diff --git a/csrc/launcher-item.h b/csrc/launcher-item.h new file mode 100644 index 0000000..b2d274a --- /dev/null +++ b/csrc/launcher-item.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_LAUNCHER_ITEM foobar_launcher_item_get_type( ) + +G_DECLARE_INTERFACE( FoobarLauncherItem, foobar_launcher_item, FOOBAR, LAUNCHER_ITEM, GObject ) + +struct _FoobarLauncherItemInterface +{ + GTypeInterface parent_interface; + + gchar const* ( *get_title ) ( FoobarLauncherItem* self ); + gchar const* ( *get_description )( FoobarLauncherItem* self ); + GIcon* ( *get_icon ) ( FoobarLauncherItem* self ); + void ( *activate ) ( FoobarLauncherItem* self ); +}; + +gchar const* foobar_launcher_item_get_title ( FoobarLauncherItem* self ); +gchar const* foobar_launcher_item_get_description( FoobarLauncherItem* self ); +GIcon* foobar_launcher_item_get_icon ( FoobarLauncherItem* self ); +void foobar_launcher_item_activate ( FoobarLauncherItem* self ); + +G_END_DECLS + diff --git a/csrc/launcher.c b/csrc/launcher.c new file mode 100644 index 0000000..f4b34d8 --- /dev/null +++ b/csrc/launcher.c @@ -0,0 +1,648 @@ +#include "launcher.h" +#include "launcher-item.h" +#include "widgets/limit-container.h" +#include +#include +#include + +// +// FoobarLauncher: +// +// A window allowing the user to quickly search for applications and launch them. +// +// Note that it should be possible to continue typing, even when the results list view is focused. Conversely, the arrow +// keys can be used to select an item, even when the search input is focused. +// + +struct _FoobarLauncher +{ + GtkWindow parent_instance; + gchar** search_terms; + GtkWidget* search_text; + GtkWidget* list_view; + GtkWidget* limit_container; + GtkFilterListModel* filter_model; + GListStore* quick_answer_model; + GtkFlattenListModel* flatten_model; + GtkSingleSelection* selection_model; + FoobarApplicationService* application_service; + FoobarQuickAnswerService* quick_answer_service; + FoobarConfigurationService* configuration_service; + gulong config_handler_id; +}; + +static void foobar_launcher_class_init ( FoobarLauncherClass* klass ); +static void foobar_launcher_init ( FoobarLauncher* self ); +static void foobar_launcher_finalize ( GObject* object ); +static void foobar_launcher_handle_search_changed ( GtkEditable* editable, + gpointer userdata ); +static void foobar_launcher_handle_search_activate ( GtkText* text, + gpointer userdata ); +static gboolean foobar_launcher_handle_search_key ( GtkEventControllerKey* controller, + guint keyval, + guint keycode, + GdkModifierType state, + gpointer userdata ); +static gboolean foobar_launcher_handle_list_key ( GtkEventControllerKey* controller, + guint keyval, + guint keycode, + GdkModifierType state, + gpointer userdata ); +static gboolean foobar_launcher_handle_window_key ( GtkEventControllerKey* controller, + guint keyval, + guint keycode, + GdkModifierType state, + gpointer userdata ); +static void foobar_launcher_handle_item_setup ( GtkListItemFactory* factory, + GtkListItem* list_item, + gpointer userdata ); +static void foobar_launcher_handle_item_activate ( GtkListView* view, + guint position, + gpointer userdata ); +static void foobar_launcher_handle_config_change ( GObject* object, + GParamSpec* pspec, + gpointer userdata ); +static void foobar_launcher_handle_show ( GtkWidget* widget, + gpointer userdata ); +static gboolean foobar_launcher_compute_icon_visible ( GtkExpression* expression, + GIcon* icon, + gpointer userdata ); +static gboolean foobar_launcher_compute_label_visible ( GtkExpression* expression, + gchar const* label, + gpointer userdata ); +static gboolean foobar_launcher_compute_separator_visible( GtkExpression* expression, + guint item_count, + gpointer userdata ); +static gboolean foobar_launcher_filter_func ( gpointer item, + gpointer userdata ); +static gboolean foobar_launcher_is_navigation_key ( guint keyval ); + +G_DEFINE_FINAL_TYPE( FoobarLauncher, foobar_launcher, GTK_TYPE_WINDOW ) + +// --------------------------------------------------------------------------------------------------------------------- +// Window Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the launcher. +// +void foobar_launcher_class_init( FoobarLauncherClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->finalize = foobar_launcher_finalize; +} + +// +// Instance initialization for the launcher. +// +void foobar_launcher_init( FoobarLauncher* self ) +{ + self->search_terms = g_new0( gchar*, 1 ); + + // Set up the search input and an event controller for auto-switching focus to the result list. + + GtkEventController* search_controller = gtk_event_controller_key_new( ); + g_signal_connect( search_controller, "key-pressed", G_CALLBACK( foobar_launcher_handle_search_key ), self ); + g_signal_connect( search_controller, "key-released", G_CALLBACK( foobar_launcher_handle_search_key ), self ); + + self->search_text = gtk_text_new( ); + gtk_text_set_placeholder_text( GTK_TEXT( self->search_text ), "Search…" ); + gtk_widget_add_controller( self->search_text, search_controller ); + gtk_widget_set_hexpand( self->search_text, TRUE ); + g_signal_connect( self->search_text, "changed", G_CALLBACK( foobar_launcher_handle_search_changed ), self ); + g_signal_connect( self->search_text, "activate", G_CALLBACK( foobar_launcher_handle_search_activate ), self ); + + GtkWidget* search_icon = gtk_image_new_from_icon_name( "fluent-search-symbolic" ); + + GtkWidget* search = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); + gtk_box_append( GTK_BOX( search ), search_icon ); + gtk_box_append( GTK_BOX( search ), self->search_text ); + gtk_widget_add_css_class( search, "search" ); + + // Set up the separator (only shown if the results are not empty). + + GtkWidget *separator = gtk_separator_new( GTK_ORIENTATION_HORIZONTAL ); + + // Set up the results list view and an event controller for auto-switching focus to the input. + + GtkListItemFactory* item_factory = gtk_signal_list_item_factory_new( ); + g_signal_connect( item_factory, "setup", G_CALLBACK( foobar_launcher_handle_item_setup ), NULL ); + + GtkCustomFilter* filter = gtk_custom_filter_new( foobar_launcher_filter_func, self, NULL ); + + self->filter_model = gtk_filter_list_model_new( NULL, GTK_FILTER( filter ) ); + + self->quick_answer_model = g_list_store_new( FOOBAR_TYPE_LAUNCHER_ITEM ); + + GListStore* flattened_models = g_list_store_new( G_TYPE_LIST_MODEL ); + g_list_store_append( flattened_models, self->quick_answer_model ); + g_list_store_append( flattened_models, self->filter_model ); + + self->flatten_model = gtk_flatten_list_model_new( G_LIST_MODEL( flattened_models ) ); + + self->selection_model = gtk_single_selection_new( G_LIST_MODEL( g_object_ref( self->flatten_model ) ) ); + + { + GtkExpression* list_expr = gtk_constant_expression_new( GTK_TYPE_FLATTEN_LIST_MODEL, self->flatten_model ); + GtkExpression* count_expr = gtk_property_expression_new( GTK_TYPE_FLATTEN_LIST_MODEL, list_expr, "n-items" ); + GtkExpression* visible_params[] = { count_expr }; + GtkExpression* visible_expr = gtk_cclosure_expression_new( + G_TYPE_BOOLEAN, + NULL, + G_N_ELEMENTS( visible_params ), + visible_params, + G_CALLBACK( foobar_launcher_compute_separator_visible ), + NULL, + NULL ); + gtk_expression_bind( visible_expr, separator, "visible", NULL ); + } + + GtkEventController* list_controller = gtk_event_controller_key_new( ); + g_signal_connect( list_controller, "key-pressed", G_CALLBACK( foobar_launcher_handle_list_key ), self ); + g_signal_connect( list_controller, "key-released", G_CALLBACK( foobar_launcher_handle_list_key ), self ); + + self->list_view = gtk_list_view_new( GTK_SELECTION_MODEL( g_object_ref( self->selection_model ) ), item_factory ); + gtk_list_view_set_single_click_activate( GTK_LIST_VIEW( self->list_view ), TRUE ); + gtk_scrollable_set_vscroll_policy( GTK_SCROLLABLE( self->list_view ), GTK_SCROLL_MINIMUM ); + gtk_widget_add_controller( self->list_view, list_controller ); + g_signal_connect( self->list_view, "activate", G_CALLBACK( foobar_launcher_handle_item_activate ), self ); + + GtkWidget* scrolled_window = gtk_scrolled_window_new( ); + // XXX: Use GTK_POLICY_AUTOMATIC instead of GTK_POLICY_EXTERNAL once I figured out how to remove the minimum + // height in this case. For now, no scrollbar is shown. + gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scrolled_window ), GTK_POLICY_NEVER, GTK_POLICY_EXTERNAL ); + gtk_scrolled_window_set_child( GTK_SCROLLED_WINDOW( scrolled_window ), self->list_view ); + gtk_scrolled_window_set_propagate_natural_height( GTK_SCROLLED_WINDOW( scrolled_window ), TRUE ); + gtk_widget_set_vexpand( scrolled_window, TRUE ); + + // Set up the layout. + + GtkWidget* layout = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); + gtk_box_append( GTK_BOX( layout ), search ); + gtk_box_append( GTK_BOX( layout ), separator ); + gtk_box_append( GTK_BOX( layout ), scrolled_window ); + + // Let the launcher take up space to accommodate the result list's natural height until the configured maximum + // height. + + self->limit_container = foobar_limit_container_new( ); + foobar_limit_container_set_child( FOOBAR_LIMIT_CONTAINER( self->limit_container ), layout ); + + // Set up the window, listening for the "Escape" key. + + GtkEventController* window_controller = gtk_event_controller_key_new( ); + g_signal_connect( window_controller, "key-released", G_CALLBACK( foobar_launcher_handle_window_key ), self ); + + gtk_window_set_child( GTK_WINDOW( self ), self->limit_container ); + gtk_widget_add_controller( GTK_WIDGET( self ), window_controller ); + gtk_window_set_title( GTK_WINDOW( self ), "Foobar Launcher" ); + gtk_widget_add_css_class( GTK_WIDGET( self ), "launcher" ); + gtk_layer_init_for_window( GTK_WINDOW( self ) ); + gtk_layer_set_keyboard_mode( GTK_WINDOW( self ), GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE ); + gtk_layer_set_namespace( GTK_WINDOW( self ), "foobar-launcher" ); + gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, TRUE ); + g_signal_connect( self, "show", G_CALLBACK( foobar_launcher_handle_show ), self ); +} + +// +// Instance cleanup for the launcher. +// +void foobar_launcher_finalize( GObject* object ) +{ + FoobarLauncher* self = (FoobarLauncher*)object; + + g_clear_signal_handler( &self->config_handler_id, self->configuration_service ); + g_clear_object( &self->filter_model ); + g_clear_object( &self->quick_answer_model ); + g_clear_object( &self->flatten_model ); + g_clear_object( &self->selection_model ); + g_clear_object( &self->application_service ); + g_clear_object( &self->quick_answer_service ); + g_clear_object( &self->configuration_service ); + g_clear_pointer( &self->search_terms, g_strfreev ); + + G_OBJECT_CLASS( foobar_launcher_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new launcher instance. +// +FoobarLauncher* foobar_launcher_new( + FoobarApplicationService* application_service, + FoobarQuickAnswerService* quick_answer_service, + FoobarConfigurationService* configuration_service ) +{ + g_return_val_if_fail( FOOBAR_IS_APPLICATION_SERVICE( application_service ), NULL ); + g_return_val_if_fail( FOOBAR_IS_QUICK_ANSWER_SERVICE( quick_answer_service ), NULL ); + g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( configuration_service ), NULL ); + + FoobarLauncher* self = g_object_new( FOOBAR_TYPE_LAUNCHER, NULL ); + self->application_service = g_object_ref( application_service ); + self->quick_answer_service = g_object_ref( quick_answer_service ); + self->configuration_service = g_object_ref( configuration_service ); + + // Set up the result list view's source model. + + GListModel* source_model = foobar_application_service_get_items( self->application_service ); + gtk_filter_list_model_set_model( self->filter_model, g_object_ref( source_model ) ); + + // Apply the configuration and subscribe to changes. + + FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); + foobar_launcher_apply_configuration( self, foobar_configuration_get_launcher( config ) ); + self->config_handler_id = g_signal_connect( + self->configuration_service, + "notify::current", + G_CALLBACK( foobar_launcher_handle_config_change ), + self ); + + return self; +} + +// +// Apply the launcher configuration provided by the configuration service. +// +void foobar_launcher_apply_configuration( + FoobarLauncher* self, + FoobarLauncherConfiguration const* config ) +{ + g_return_if_fail( FOOBAR_IS_LAUNCHER( self ) ); + g_return_if_fail( config != NULL ); + + gtk_window_set_default_size( GTK_WINDOW( self ), foobar_launcher_configuration_get_width( config ), 2 ); + gtk_layer_set_margin( + GTK_WINDOW( self ), + GTK_LAYER_SHELL_EDGE_TOP, + foobar_launcher_configuration_get_position( config ) ); + foobar_limit_container_set_max_height( + FOOBAR_LIMIT_CONTAINER( self->limit_container ), + foobar_launcher_configuration_get_max_height( config ) ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called when the user confirms the selection while the search input is focused. +// +// We forward the activation to the results list view. +// +void foobar_launcher_handle_search_activate( + GtkText* text, + gpointer userdata ) +{ + (void)text; + FoobarLauncher* self = (FoobarLauncher*)userdata; + + guint selection = gtk_single_selection_get_selected( self->selection_model ); + if ( selection != GTK_INVALID_LIST_POSITION ) + { + gtk_widget_activate_action( self->list_view, "list.activate-item", "u", selection ); + } +} + +// +// Called when the search query has changed. +// +// We update the tokenized search terms and then the result filter. +// +void foobar_launcher_handle_search_changed( + GtkEditable* editable, + gpointer userdata ) +{ + FoobarLauncher* self = (FoobarLauncher*)userdata; + + // Update quick answer. + + g_autoptr(FoobarQuickAnswer) answer = foobar_quick_answer_service_query( self->quick_answer_service, gtk_editable_get_text( editable ) ); + g_list_store_remove_all( self->quick_answer_model ); + if (answer) + { + g_list_store_append(self->quick_answer_model, answer); + } + + // Update filter. + + GStrvBuilder* terms_builder = g_strv_builder_new( ); + g_autofree gchar* query = g_strdup( gtk_editable_get_text( editable ) ); + gchar* query_start = query; + gchar* save; + gchar* token; + while ( ( token = strtok_r( query_start, " \t", &save ) ) ) + { + if ( *token ) { g_strv_builder_add( terms_builder, token ); } + query_start = NULL; + } + + g_clear_pointer( &self->search_terms, g_strfreev ); + self->search_terms = g_strv_builder_end( terms_builder ); + + GtkFilter* filter = gtk_filter_list_model_get_filter( self->filter_model ); + gtk_filter_changed( filter, GTK_FILTER_CHANGE_DIFFERENT ); +} + +// +// Handle keyboard events while the search input is focused. +// +// If this is a navigational key, the event is forwarded to the results list view. +// +gboolean foobar_launcher_handle_search_key( + GtkEventControllerKey* controller, + guint keyval, + guint keycode, + GdkModifierType state, + gpointer userdata ) +{ + (void)keycode; + (void)state; + FoobarLauncher* self = (FoobarLauncher*)userdata; + + if ( foobar_launcher_is_navigation_key( keyval ) ) + { + gtk_widget_grab_focus( self->list_view ); + return gtk_event_controller_key_forward( controller, self->list_view ); + } + else + { + return FALSE; + } +} + +// +// Handle keyboard events while the list view is focused. +// +// If this is not a navigational key, the event is forwarded to the search input, which is also focused. +// +gboolean foobar_launcher_handle_list_key( + GtkEventControllerKey* controller, + guint keyval, + guint keycode, + GdkModifierType state, + gpointer userdata ) +{ + (void)keycode; + (void)state; + FoobarLauncher* self = (FoobarLauncher*)userdata; + + if ( foobar_launcher_is_navigation_key( keyval ) ) + { + return FALSE; + } + else + { + gtk_text_grab_focus_without_selecting( GTK_TEXT( self->search_text ) ); + return gtk_event_controller_key_forward( controller, self->search_text ); + } +} + +// +// Handle general keyboard events for the window. +// +// This is used to allow the user to close the launcher by pressing the "Escape" key. +// +gboolean foobar_launcher_handle_window_key( + GtkEventControllerKey* controller, + guint keyval, + guint keycode, + GdkModifierType state, + gpointer userdata ) +{ + (void)controller; + (void)keycode; + (void)state; + FoobarLauncher* self = (FoobarLauncher*)userdata; + + if ( keyval == GDK_KEY_Escape ) + { + gtk_widget_set_visible( GTK_WIDGET( self ), FALSE ); + return TRUE; + } + else + { + return FALSE; + } +} + +// +// Called by the result list view to create a widget for displaying an application item. +// +void foobar_launcher_handle_item_setup( + GtkListItemFactory* factory, + GtkListItem* list_item, + gpointer userdata ) +{ + (void)factory; + (void)userdata; + + // Set up layout. + + GtkWidget* icon = gtk_image_new( ); + gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); + gtk_widget_add_css_class( icon, "icon" ); + + GtkWidget* title = gtk_label_new( NULL ); + gtk_label_set_justify( GTK_LABEL( title ), GTK_JUSTIFY_LEFT ); + gtk_label_set_xalign( GTK_LABEL( title ), 0 ); + gtk_label_set_wrap( GTK_LABEL( title ), FALSE ); + gtk_label_set_ellipsize( GTK_LABEL( title ), PANGO_ELLIPSIZE_END ); + gtk_widget_add_css_class( title, "name" ); + + GtkWidget* description = gtk_label_new( NULL ); + gtk_label_set_justify( GTK_LABEL( description ), GTK_JUSTIFY_LEFT ); + gtk_label_set_xalign( GTK_LABEL( description ), 0 ); + gtk_label_set_wrap( GTK_LABEL( description ), FALSE ); + gtk_label_set_ellipsize( GTK_LABEL( description ), PANGO_ELLIPSIZE_END ); + gtk_widget_add_css_class( description, "description" ); + + GtkWidget* column = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); + gtk_widget_set_hexpand( column, TRUE ); + gtk_widget_set_valign( column, GTK_ALIGN_CENTER ); + gtk_box_append( GTK_BOX( column ), title ); + gtk_box_append( GTK_BOX( column ), description ); + + GtkWidget* row = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); + gtk_widget_add_css_class( row, "item" ); + gtk_box_append( GTK_BOX( row ), icon ); + gtk_box_append( GTK_BOX( row ), column ); + + gtk_list_item_set_child( list_item, row ); + + // Set up bindings. + + { + GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" ); + GtkExpression* icon_expr = gtk_property_expression_new( FOOBAR_TYPE_LAUNCHER_ITEM, item_expr, "icon" ); + gtk_expression_bind( gtk_expression_ref( icon_expr ), icon, "gicon", list_item ); + GtkExpression* visible_params[] = { icon_expr }; + GtkExpression* visible_expr = gtk_cclosure_expression_new( + G_TYPE_BOOLEAN, + NULL, + G_N_ELEMENTS( visible_params ), + visible_params, + G_CALLBACK( foobar_launcher_compute_icon_visible ), + NULL, + NULL ); + gtk_expression_bind( visible_expr, icon, "visible", list_item ); + } + + { + GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" ); + GtkExpression* title_expr = gtk_property_expression_new( FOOBAR_TYPE_LAUNCHER_ITEM, item_expr, "title" ); + gtk_expression_bind( title_expr, title, "label", list_item ); + } + + { + GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" ); + GtkExpression* description_expr = gtk_property_expression_new( FOOBAR_TYPE_LAUNCHER_ITEM, item_expr, "description" ); + gtk_expression_bind( gtk_expression_ref( description_expr ), description, "label", list_item ); + GtkExpression* visible_params[] = { description_expr }; + GtkExpression* visible_expr = gtk_cclosure_expression_new( + G_TYPE_BOOLEAN, + NULL, + G_N_ELEMENTS( visible_params ), + visible_params, + G_CALLBACK( foobar_launcher_compute_label_visible ), + NULL, + NULL ); + gtk_expression_bind( visible_expr, description, "visible", list_item ); + } +} + +// +// Called by the result list view when an application was selected. +// +void foobar_launcher_handle_item_activate( + GtkListView* view, + guint position, + gpointer userdata ) +{ + (void)view; + FoobarLauncher* self = (FoobarLauncher*)userdata; + + GListModel* list = G_LIST_MODEL( gtk_list_view_get_model( view ) ); + FoobarLauncherItem* item = g_list_model_get_item( list, position ); + foobar_launcher_item_activate( item ); + gtk_widget_set_visible( GTK_WIDGET( self ), FALSE ); +} + +// +// Signal handler called when the global configuration file has changed. +// +void foobar_launcher_handle_config_change( + GObject* object, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)object; + (void)pspec; + FoobarLauncher* self = (FoobarLauncher*)userdata; + + FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); + foobar_launcher_apply_configuration( self, foobar_configuration_get_launcher( config ) ); +} + +// +// Called before the launcher is presented. +// +// This is used to reset the UI state. +// +void foobar_launcher_handle_show( + GtkWidget* widget, + gpointer userdata ) +{ + (void)widget; + FoobarLauncher* self = (FoobarLauncher*)userdata; + + gtk_editable_set_text( GTK_EDITABLE( self->search_text ), "" ); + if ( g_list_model_get_n_items( G_LIST_MODEL( self->selection_model ) ) > 0 ) + { + gtk_list_view_scroll_to( + GTK_LIST_VIEW( self->list_view ), + 0, + GTK_LIST_SCROLL_FOCUS | GTK_LIST_SCROLL_SELECT, + NULL ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Value Converters +// --------------------------------------------------------------------------------------------------------------------- + +// +// Derive the visibility of an application icon from its value. +// +gboolean foobar_launcher_compute_icon_visible( + GtkExpression* expression, + GIcon* icon, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + return icon != NULL; +} + +// +// Derive the visibility of an application label from its value. +// +gboolean foobar_launcher_compute_label_visible( + GtkExpression* expression, + gchar const* label, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + return label != NULL; +} + +// +// Derive the visibility of the separator between search input and result list view from the number of results. +// +gboolean foobar_launcher_compute_separator_visible( + GtkExpression* expression, + guint item_count, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + return item_count > 0; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Match a result item against the current search query. +// +gboolean foobar_launcher_filter_func( gpointer item, gpointer userdata ) +{ + FoobarLauncher* self = (FoobarLauncher*)userdata; + FoobarApplicationItem* app = (FoobarApplicationItem*)item; + + return foobar_application_item_match( app, (gchar const* const*)self->search_terms ); +} + +// +// Check if a key value is that of a navigation key (i.e., an arrow key). +// +gboolean foobar_launcher_is_navigation_key( guint keyval ) +{ + switch ( keyval ) + { + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_Left: + case GDK_KEY_Right: + return TRUE; + default: + return FALSE; + } +} diff --git a/csrc/launcher.h b/csrc/launcher.h new file mode 100644 index 0000000..5d92607 --- /dev/null +++ b/csrc/launcher.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include "services/application-service.h" +#include "services/configuration-service.h" +#include "services/quick-answer-service.h" + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_LAUNCHER foobar_launcher_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarLauncher, foobar_launcher, FOOBAR, LAUNCHER, GtkWindow ) + +FoobarLauncher* foobar_launcher_new ( FoobarApplicationService* application_service, + FoobarQuickAnswerService* quick_answer_service, + FoobarConfigurationService* configuration_service ); +void foobar_launcher_apply_configuration( FoobarLauncher* self, + FoobarLauncherConfiguration const* config ); + +G_END_DECLS diff --git a/csrc/main.c b/csrc/main.c new file mode 100644 index 0000000..acabbdc --- /dev/null +++ b/csrc/main.c @@ -0,0 +1,12 @@ +#include "application.h" + +// +// Entry point of the application, runs the GTK app. +// +int main( + int argc, + char** argv ) +{ + g_autoptr( FoobarApplication ) app = foobar_application_new( ); + return g_application_run( G_APPLICATION( app ), argc, argv ); +} diff --git a/csrc/meson.build b/csrc/meson.build new file mode 100644 index 0000000..b9383fb --- /dev/null +++ b/csrc/meson.build @@ -0,0 +1,74 @@ +foobar_sources = files( + 'application.c', + 'utils.c', + 'panel.c', + 'launcher.c', + 'launcher-item.c', + 'control-center.c', + 'notification-area.c', +) +foobar_tests = {} + +subdir('dbus') +subdir('services') +subdir('widgets') + +c_args = [ + '-Wall', + '-Werror', + '-Wswitch-enum', + '-Wswitch-default', + '-Wunused', + '-Wuninitialized', +] + +deps = [ + glib_dep, + gio_dep, + gio_unix_dep, + gtk_dep, + json_dep, + gmp_dep, + nm_dep, + m_dep, + layershell_dep, + gvc_dep, +] + +libfoobar = static_library( + meson.project_name(), + foobar_sources, + include_directories: include_directories('.'), + dependencies: deps, + install: false, + c_args: c_args +) + +libfoobar_dep = declare_dependency( + link_with: libfoobar, + include_directories: include_directories('.'), + dependencies: deps, +) + +executable( + meson.project_name(), + 'main.c', + styles_resource, + icons_resource, + dependencies: [ libfoobar_dep ], + install: true, + c_args: c_args +) + +if mutest_dep.found() + foreach name, sources : foobar_tests + test( + name, + executable( + 'foobar-test-' + name, + sources, + dependencies: [ libfoobar_dep, mutest_dep ], + ), + ) + endforeach +endif diff --git a/csrc/notification-area.c b/csrc/notification-area.c new file mode 100644 index 0000000..2db5397 --- /dev/null +++ b/csrc/notification-area.c @@ -0,0 +1,196 @@ +#include "notification-area.h" +#include "widgets/notification-widget.h" +#include + +// +// FoobarNotificationArea: +// +// A (usually invisible) window in the corner of the screen, displaying incoming notifications before they are either +// dismissed or a timeout has passed. +// + +struct _FoobarNotificationArea +{ + GtkWindow parent_instance; + GtkWidget* notification_list; + FoobarNotificationService* notification_service; + FoobarConfigurationService* configuration_service; + gint min_height; + gint spacing; + gint close_button_inset; + gchar* time_format; + gulong config_handler_id; +}; + +static void foobar_notification_area_class_init ( FoobarNotificationAreaClass* klass ); +static void foobar_notification_area_init ( FoobarNotificationArea* self ); +static void foobar_notification_area_finalize ( GObject* object ); +static void foobar_notification_area_handle_config_change( GObject* object, + GParamSpec* pspec, + gpointer userdata ); +static void foobar_notification_area_handle_item_setup ( GtkListItemFactory* factory, + GtkListItem* list_item, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarNotificationArea, foobar_notification_area, GTK_TYPE_WINDOW ) + +// --------------------------------------------------------------------------------------------------------------------- +// Window Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the notification area. +// +void foobar_notification_area_class_init( FoobarNotificationAreaClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->finalize = foobar_notification_area_finalize; +} + +// +// Instance initialization for the notification area. +// +void foobar_notification_area_init( FoobarNotificationArea* self ) +{ + GtkListItemFactory* notification_factory = gtk_signal_list_item_factory_new( ); + g_signal_connect( notification_factory, "setup", G_CALLBACK( foobar_notification_area_handle_item_setup ), self ); + self->notification_list = gtk_list_view_new( NULL, notification_factory ); + + gtk_window_set_child( GTK_WINDOW( self ), self->notification_list ); + gtk_window_set_title( GTK_WINDOW( self ), "Foobar Notification Area" ); + gtk_widget_add_css_class( GTK_WIDGET( self ), "notification-area" ); + gtk_layer_init_for_window( GTK_WINDOW( self ) ); + gtk_layer_set_namespace( GTK_WINDOW( self ), "foobar-notification-area" ); + gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_RIGHT, TRUE ); + gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, TRUE ); +} + +// +// Instance cleanup for the notification area. +// +void foobar_notification_area_finalize( GObject* object ) +{ + FoobarNotificationArea* self = (FoobarNotificationArea*)object; + + g_clear_signal_handler( &self->config_handler_id, self->configuration_service ); + g_clear_object( &self->notification_service ); + g_clear_object( &self->configuration_service ); + g_clear_pointer( &self->time_format, g_free ); + + G_OBJECT_CLASS( foobar_notification_area_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new notification area instance. +// +FoobarNotificationArea* foobar_notification_area_new( + FoobarNotificationService* notification_service, + FoobarConfigurationService* configuration_service ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( notification_service ), NULL ); + g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( configuration_service ), NULL ); + + FoobarNotificationArea* self = g_object_new( FOOBAR_TYPE_NOTIFICATION_AREA, NULL ); + 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_notification_area_apply_configuration( self, foobar_configuration_get_notifications( config ) ); + self->config_handler_id = g_signal_connect( + self->configuration_service, + "notify::current", + G_CALLBACK( foobar_notification_area_handle_config_change ), + self ); + + // Set up the notifications list view. + + GListModel* source_model = foobar_notification_service_get_popup_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 ) ); + + return self; +} + +// +// Apply the notification configuration provided by the configuration service. +// +void foobar_notification_area_apply_configuration( + FoobarNotificationArea* self, + FoobarNotificationConfiguration const* config ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_AREA( self ) ); + g_return_if_fail( config != NULL ); + + // Copy configuration into member variables. + + gint width = foobar_notification_configuration_get_width( config ); + self->min_height = foobar_notification_configuration_get_min_height( config ); + self->spacing = foobar_notification_configuration_get_spacing( config ); + self->close_button_inset = foobar_notification_configuration_get_close_button_inset( config ); + g_clear_pointer( &self->time_format, g_free ); + self->time_format = g_strdup( foobar_notification_configuration_get_time_format( config ) ); + + // Recreate 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 ); + + // We need to use height 2, otherwise it crashes for some reason. + + gtk_window_set_default_size( GTK_WINDOW( self ), width + self->spacing, 2 ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Signal handler called when the global configuration file has changed. +// +void foobar_notification_area_handle_config_change( + GObject* object, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)object; + (void)pspec; + FoobarNotificationArea* self = (FoobarNotificationArea*)userdata; + + FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); + foobar_notification_area_apply_configuration( self, foobar_configuration_get_notifications( config ) ); +} + +// +// Called by the notification list view to create a widget for displaying a notification. +// +void foobar_notification_area_handle_item_setup( + GtkListItemFactory* factory, + GtkListItem* list_item, + gpointer userdata ) +{ + (void)factory; + FoobarNotificationArea* self = (FoobarNotificationArea*)userdata; + + GtkWidget* widget = foobar_notification_widget_new( ); + foobar_notification_widget_set_close_action( FOOBAR_NOTIFICATION_WIDGET( widget ), FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS ); + foobar_notification_widget_set_time_format( FOOBAR_NOTIFICATION_WIDGET( widget ), self->time_format ); + foobar_notification_widget_set_min_height( FOOBAR_NOTIFICATION_WIDGET( widget ), self->min_height ); + foobar_notification_widget_set_close_button_inset( FOOBAR_NOTIFICATION_WIDGET( widget ), self->close_button_inset ); + foobar_notification_widget_set_inset_end( FOOBAR_NOTIFICATION_WIDGET( widget ), self->spacing ); + foobar_notification_widget_set_inset_top( FOOBAR_NOTIFICATION_WIDGET( widget ), self->spacing ); + 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 ); + } +} diff --git a/csrc/notification-area.h b/csrc/notification-area.h new file mode 100644 index 0000000..e2e546f --- /dev/null +++ b/csrc/notification-area.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include "services/notification-service.h" +#include "services/configuration-service.h" + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_NOTIFICATION_AREA foobar_notification_area_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarNotificationArea, foobar_notification_area, FOOBAR, NOTIFICATION_AREA, GtkWindow ) + +FoobarNotificationArea* foobar_notification_area_new ( FoobarNotificationService* notification_service, + FoobarConfigurationService* configuration_service ); +void foobar_notification_area_apply_configuration( FoobarNotificationArea* self, + FoobarNotificationConfiguration const* config ); + +G_END_DECLS diff --git a/csrc/panel.c b/csrc/panel.c new file mode 100644 index 0000000..5bc7662 --- /dev/null +++ b/csrc/panel.c @@ -0,0 +1,348 @@ +#include "panel.h" +#include "widgets/panel/panel-item-icon.h" +#include "widgets/panel/panel-item-clock.h" +#include "widgets/panel/panel-item-workspaces.h" +#include "widgets/panel/panel-item-status.h" +#include + +// +// FoobarPanel: +// +// A panel that is divided into start, center and end sections. Each section can be configured to present a range of +// items. +// + +struct _FoobarPanel +{ + GtkWindow parent_instance; + GtkWidget* main_layout; + GtkWidget* start_items; + GtkWidget* center_items; + GtkWidget* end_items; + GPtrArray* item_widgets; + GdkMonitor* monitor; + FoobarBatteryService* battery_service; + FoobarClockService* clock_service; + FoobarBrightnessService* brightness_service; + FoobarWorkspaceService* workspace_service; + FoobarAudioService* audio_service; + FoobarNetworkService* network_service; + FoobarBluetoothService* bluetooth_service; + FoobarNotificationService* notification_service; + FoobarConfigurationService* configuration_service; + gulong config_handler_id; +}; + +static void foobar_panel_class_init ( FoobarPanelClass* klass ); +static void foobar_panel_init ( FoobarPanel* self ); +static void foobar_panel_finalize ( GObject* object ); +static void foobar_panel_handle_config_change( GObject* object, + GParamSpec* pspec, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarPanel, foobar_panel, GTK_TYPE_APPLICATION_WINDOW ) + +// --------------------------------------------------------------------------------------------------------------------- +// Window Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the panel. +// +void foobar_panel_class_init( FoobarPanelClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->finalize = foobar_panel_finalize; +} + +// +// Instance initialization for the panel. +// +void foobar_panel_init( FoobarPanel* self ) +{ + self->start_items = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); + self->center_items = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); + self->end_items = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); + + self->main_layout = gtk_center_box_new( ); + gtk_center_box_set_start_widget( GTK_CENTER_BOX( self->main_layout ), self->start_items ); + gtk_center_box_set_center_widget( GTK_CENTER_BOX( self->main_layout ), self->center_items ); + gtk_center_box_set_end_widget( GTK_CENTER_BOX( self->main_layout ), self->end_items ); + + gtk_window_set_child( GTK_WINDOW( self ), self->main_layout ); + gtk_window_set_title( GTK_WINDOW( self ), "Foobar Panel" ); + gtk_widget_add_css_class( GTK_WIDGET( self ), "panel" ); + gtk_layer_init_for_window( GTK_WINDOW( self ) ); + gtk_layer_set_namespace( GTK_WINDOW( self ), "foobar-panel" ); + gtk_layer_auto_exclusive_zone_enable( GTK_WINDOW( self ) ); +} + +// +// Instance cleanup for the panel. +// +void foobar_panel_finalize( GObject* object ) +{ + FoobarPanel* self = (FoobarPanel*)object; + + g_clear_signal_handler( &self->config_handler_id, self->configuration_service ); + g_clear_object( &self->monitor ); + g_clear_object( &self->battery_service ); + g_clear_object( &self->clock_service ); + g_clear_object( &self->brightness_service ); + g_clear_object( &self->workspace_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_object( &self->configuration_service ); + g_clear_pointer( &self->item_widgets, g_ptr_array_unref ); + + G_OBJECT_CLASS( foobar_panel_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new panel instance. +// +// An optional GdkMonitor instance can be provided for multi-monitor setups. Otherwise, the default monitor is used. +// +FoobarPanel* foobar_panel_new( + GtkApplication* app, + GdkMonitor* monitor, + FoobarBatteryService* battery_service, + FoobarClockService* clock_service, + FoobarBrightnessService* brightness_service, + FoobarWorkspaceService* workspace_service, + FoobarAudioService* audio_service, + FoobarNetworkService* network_service, + FoobarBluetoothService* bluetooth_service, + FoobarNotificationService* notification_service, + FoobarConfigurationService* configuration_service ) +{ + g_return_val_if_fail( GTK_IS_APPLICATION( app ), NULL ); + g_return_val_if_fail( monitor == NULL || GDK_IS_MONITOR( monitor ), NULL ); + g_return_val_if_fail( FOOBAR_IS_BATTERY_SERVICE( battery_service ), NULL ); + g_return_val_if_fail( FOOBAR_IS_CLOCK_SERVICE( clock_service ), NULL ); + g_return_val_if_fail( FOOBAR_IS_BRIGHTNESS_SERVICE( brightness_service ), NULL ); + g_return_val_if_fail( FOOBAR_IS_WORKSPACE_SERVICE( workspace_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 ); + + FoobarPanel* self = g_object_new( FOOBAR_TYPE_PANEL, "application", app, NULL ); + self->battery_service = g_object_ref( battery_service ); + self->clock_service = g_object_ref( clock_service ); + self->brightness_service = g_object_ref( brightness_service ); + self->workspace_service = g_object_ref( workspace_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 ); + + // Set the monitor. + + if ( monitor ) + { + self->monitor = g_object_ref( monitor ); + gtk_layer_set_monitor( GTK_WINDOW( self ), self->monitor ); + } + + // Apply the configuration and subscribe to changes. + + FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); + foobar_panel_apply_configuration( self, foobar_configuration_get_panel( config ) ); + self->config_handler_id = g_signal_connect( + self->configuration_service, + "notify::current", + G_CALLBACK( foobar_panel_handle_config_change ), + self ); + + return self; +} + +// +// Apply the panel configuration provided by the configuration service. +// +void foobar_panel_apply_configuration( + FoobarPanel* self, + FoobarPanelConfiguration const* config ) +{ + g_return_if_fail( FOOBAR_IS_PANEL( self ) ); + g_return_if_fail( config != NULL ); + + // Derive the orientation from the panel's position. + + FoobarScreenEdge position = foobar_panel_configuration_get_position( config ); + GtkOrientation orientation; + gchar const* orientation_css_class; + switch ( position ) + { + case FOOBAR_SCREEN_EDGE_LEFT: + case FOOBAR_SCREEN_EDGE_RIGHT: + orientation = GTK_ORIENTATION_VERTICAL; + orientation_css_class = "vertical"; + break; + case FOOBAR_SCREEN_EDGE_TOP: + case FOOBAR_SCREEN_EDGE_BOTTOM: + orientation = GTK_ORIENTATION_HORIZONTAL; + orientation_css_class = "horizontal"; + break; + default: + g_warn_if_reached( ); + return; + } + + // Configure the window. + + gtk_widget_remove_css_class( GTK_WIDGET( self ), "horizontal" ); + gtk_widget_remove_css_class( GTK_WIDGET( self ), "vertical" ); + gtk_widget_add_css_class( GTK_WIDGET( self ), orientation_css_class ); + + gint size = foobar_panel_configuration_get_size( config ); + gtk_window_set_default_size( + GTK_WINDOW( self ), + orientation == GTK_ORIENTATION_VERTICAL ? size : -1, + orientation == GTK_ORIENTATION_HORIZONTAL ? size : -1 ); + + gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_LEFT, position != FOOBAR_SCREEN_EDGE_RIGHT ); + gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_RIGHT, position != FOOBAR_SCREEN_EDGE_LEFT ); + gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, position != FOOBAR_SCREEN_EDGE_BOTTOM ); + gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_BOTTOM, position != FOOBAR_SCREEN_EDGE_TOP ); + + // Configure layout spacing. + + gint margin = foobar_panel_configuration_get_margin( config ); + gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_LEFT, position != FOOBAR_SCREEN_EDGE_RIGHT ? margin : 0 ); + gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_RIGHT, position != FOOBAR_SCREEN_EDGE_LEFT ? margin : 0 ); + gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, position != FOOBAR_SCREEN_EDGE_BOTTOM ? margin : 0 ); + gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_BOTTOM, position != FOOBAR_SCREEN_EDGE_TOP ? margin : 0 ); + + gint padding = foobar_panel_configuration_get_padding( config ); + gtk_widget_set_margin_start( GTK_WIDGET( self->main_layout ), orientation == GTK_ORIENTATION_HORIZONTAL ? padding : 0 ); + gtk_widget_set_margin_end( GTK_WIDGET( self->main_layout ), orientation == GTK_ORIENTATION_HORIZONTAL ? padding : 0 ); + gtk_widget_set_margin_top( GTK_WIDGET( self->main_layout ), orientation == GTK_ORIENTATION_VERTICAL ? padding : 0 ); + gtk_widget_set_margin_bottom( GTK_WIDGET( self->main_layout ), orientation == GTK_ORIENTATION_VERTICAL ? padding : 0 ); + + gint spacing = foobar_panel_configuration_get_spacing( config ); + gtk_box_set_spacing( GTK_BOX(self->start_items), spacing ); + gtk_box_set_spacing( GTK_BOX(self->center_items), spacing ); + gtk_box_set_spacing( GTK_BOX(self->end_items), spacing ); + gtk_widget_set_margin_top( GTK_WIDGET( self->center_items ), orientation == GTK_ORIENTATION_VERTICAL ? spacing : 0 ); + gtk_widget_set_margin_bottom( GTK_WIDGET( self->center_items ), orientation == GTK_ORIENTATION_VERTICAL ? spacing : 0 ); + gtk_widget_set_margin_start( GTK_WIDGET( self->center_items ), orientation == GTK_ORIENTATION_HORIZONTAL ? spacing : 0 ); + gtk_widget_set_margin_end( GTK_WIDGET( self->center_items ), orientation == GTK_ORIENTATION_HORIZONTAL ? spacing : 0 ); + + // Configure the container orientations. + + gtk_orientable_set_orientation( GTK_ORIENTABLE( self->main_layout ), orientation ); + gtk_orientable_set_orientation( GTK_ORIENTABLE( self->start_items ), orientation ); + gtk_orientable_set_orientation( GTK_ORIENTABLE( self->center_items ), orientation ); + gtk_orientable_set_orientation( GTK_ORIENTABLE( self->end_items ), orientation ); + + // Re-create panel items. + + if ( self->item_widgets ) + { + for ( guint i = 0; i < self->item_widgets->len; ++i ) + { + gtk_widget_unparent( g_ptr_array_index( self->item_widgets, i ) ); + } + g_clear_pointer( &self->item_widgets, g_ptr_array_unref ); + } + + gsize items_count; + FoobarPanelItemConfiguration const* const* items = foobar_panel_configuration_get_items( config, &items_count ); + self->item_widgets = g_ptr_array_sized_new( items_count ); + for ( gsize i = 0; i < items_count; ++i ) + { + // Create an item for the specified type and configuration. + + FoobarPanelItemConfiguration const* item = items[i]; + FoobarPanelItem* item_widget; + switch ( foobar_panel_item_configuration_get_kind( item ) ) + { + case FOOBAR_PANEL_ITEM_KIND_ICON: + item_widget = foobar_panel_item_icon_new( item ); + break; + case FOOBAR_PANEL_ITEM_KIND_CLOCK: + item_widget = foobar_panel_item_clock_new( + item, + self->clock_service ); + break; + case FOOBAR_PANEL_ITEM_KIND_WORKSPACES: + item_widget = foobar_panel_item_workspaces_new( + item, + self->monitor, + self->workspace_service ); + break; + case FOOBAR_PANEL_ITEM_KIND_STATUS: + item_widget = foobar_panel_item_status_new( + item, + self->battery_service, + self->brightness_service, + self->audio_service, + self->network_service, + self->bluetooth_service, + self->notification_service ); + break; + default: + g_warn_if_reached( ); + continue; + } + + // Propagate the panel's orientation if the item supports it. + + if ( GTK_IS_ORIENTABLE( item_widget ) ) + { + gtk_orientable_set_orientation( GTK_ORIENTABLE( item_widget ), orientation ); + } + + // Add the item to the correct container, depending on its configured position. + + switch ( foobar_panel_item_configuration_get_position( item ) ) + { + case FOOBAR_PANEL_ITEM_POSITION_START: + gtk_box_append( GTK_BOX( self->start_items ), GTK_WIDGET( item_widget ) ); + break; + case FOOBAR_PANEL_ITEM_POSITION_CENTER: + gtk_box_append( GTK_BOX( self->center_items ), GTK_WIDGET( item_widget ) ); + break; + case FOOBAR_PANEL_ITEM_POSITION_END: + gtk_box_append( GTK_BOX( self->end_items ), GTK_WIDGET( item_widget ) ); + break; + default: + g_warn_if_reached( ); + break; + } + + // Add the item to the pointer array for easier cleanup. + + g_ptr_array_add( self->item_widgets, GTK_WIDGET( item_widget ) ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Signal handler called when the global configuration file has changed. +// +void foobar_panel_handle_config_change( + GObject* object, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)object; + (void)pspec; + FoobarPanel* self = (FoobarPanel*)userdata; + + FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); + foobar_panel_apply_configuration( self, foobar_configuration_get_panel( config ) ); +} diff --git a/csrc/panel.h b/csrc/panel.h new file mode 100644 index 0000000..e64a7d7 --- /dev/null +++ b/csrc/panel.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include "services/configuration-service.h" +#include "services/battery-service.h" +#include "services/clock-service.h" +#include "services/brightness-service.h" +#include "services/workspace-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 foobar_panel_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarPanel, foobar_panel, FOOBAR, PANEL, GtkApplicationWindow ) + +FoobarPanel* foobar_panel_new ( GtkApplication* app, + GdkMonitor* monitor, + FoobarBatteryService* battery_service, + FoobarClockService* clock_service, + FoobarBrightnessService* brightness_service, + FoobarWorkspaceService* workspace_service, + FoobarAudioService* audio_service, + FoobarNetworkService* network_service, + FoobarBluetoothService* bluetooth_service, + FoobarNotificationService* notification_service, + FoobarConfigurationService* configuration_service ); +void foobar_panel_apply_configuration( FoobarPanel* self, + FoobarPanelConfiguration const* config ); + +G_END_DECLS diff --git a/csrc/services/application-service.c b/csrc/services/application-service.c new file mode 100644 index 0000000..42835ac --- /dev/null +++ b/csrc/services/application-service.c @@ -0,0 +1,746 @@ +#include "services/application-service.h" +#include "launcher-item.h" +#include "utils.h" +#include +#include +#include +#include + +// +// FoobarApplicationItem: +// +// Represents a single application/desktop file. +// + +struct _FoobarApplicationItem +{ + GObject parent_instance; + FoobarApplicationService* service; + GAppInfo* info; +}; + +enum +{ + APP_PROP_ID = 1, + APP_PROP_EXECUTABLE, + APP_PROP_CATEGORIES, + APP_PROP_FREQUENCY, + APP_PROP_TITLE, + APP_PROP_DESCRIPTION, + APP_PROP_ICON, + N_APP_PROPS, +}; + +static GParamSpec* app_props[N_APP_PROPS] = { 0 }; + +static void foobar_application_item_class_init ( FoobarApplicationItemClass* klass ); +static void foobar_application_item_launcher_item_interface_init( FoobarLauncherItemInterface* iface ); +static void foobar_application_item_init ( FoobarApplicationItem* self ); +static void foobar_application_item_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_application_item_finalize ( GObject* object ); +static FoobarApplicationItem* foobar_application_item_new ( FoobarApplicationService* service ); +static gchar const* foobar_application_item_get_title ( FoobarLauncherItem* self ); +static gchar const* foobar_application_item_get_description ( FoobarLauncherItem* self ); +static GIcon* foobar_application_item_get_icon ( FoobarLauncherItem* self ); +static void foobar_application_item_activate ( FoobarLauncherItem* self ); +static void foobar_application_item_set_info ( FoobarApplicationItem* self, + GAppInfo* value ); + +G_DEFINE_FINAL_TYPE_WITH_CODE( + FoobarApplicationItem, + foobar_application_item, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE( FOOBAR_TYPE_LAUNCHER_ITEM, foobar_application_item_launcher_item_interface_init ) ) + +// +// FoobarApplicationService: +// +// Service providing a list of all installed applications. This implemented using GLib's AppInfo API. +// + +struct _FoobarApplicationService +{ + GObject parent_instance; + GListStore* items; + GtkSortListModel* sorted_items; + GAppInfoMonitor* monitor; + GHashTable* frequencies; // only modified on the main thread, but possibly read on a background thread + GMutex frequencies_mutex; + GMutex write_cache_mutex; + gchar* cache_path; + gulong changed_handler_id; +}; + +enum +{ + PROP_ITEMS = 1, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_application_service_class_init ( FoobarApplicationServiceClass* klass ); +static void foobar_application_service_init ( FoobarApplicationService* self ); +static void foobar_application_service_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_application_service_finalize ( GObject* object ); +static void foobar_application_service_handle_changed ( GAppInfoMonitor* monitor, + gpointer userdata ); +static void foobar_application_service_update ( FoobarApplicationService* self ); +static void foobar_application_service_read_cache ( FoobarApplicationService* self ); +static void foobar_application_service_read_cache_foreach_cb ( JsonObject* object, + gchar const* member_name, + JsonNode* member_node, + gpointer userdata ); +static void foobar_application_service_write_cache ( FoobarApplicationService* self ); +static void foobar_application_service_write_cache_cb ( GObject* object, + GAsyncResult* result, + gpointer userdata ); +static void foobar_application_service_write_cache_async ( FoobarApplicationService* self, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer userdata ); +static gboolean foobar_application_service_write_cache_finish ( FoobarApplicationService* self, + GAsyncResult* result, + GError** error ); +static void foobar_application_service_write_cache_thread ( GTask* task, + gpointer source_object, + gpointer task_data, + GCancellable* cancellable ); +static void foobar_application_service_write_cache_foreach_cb( gpointer key, + gpointer value, + gpointer userdata ); +static gint foobar_application_service_sort_func ( gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarApplicationService, foobar_application_service, G_TYPE_OBJECT ) + +// --------------------------------------------------------------------------------------------------------------------- +// Item Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for application items. +// +void foobar_application_item_class_init( FoobarApplicationItemClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_application_item_get_property; + object_klass->finalize = foobar_application_item_finalize; + + gpointer launcher_item_iface = g_type_default_interface_peek( FOOBAR_TYPE_LAUNCHER_ITEM ); + app_props[APP_PROP_ID] = g_param_spec_string( + "id", + "ID", + "Application identifier string.", + NULL, + G_PARAM_READABLE ); + app_props[APP_PROP_EXECUTABLE] = g_param_spec_string( + "executable", + "Executable", + "The executable name of the application.", + NULL, + G_PARAM_READABLE ); + app_props[APP_PROP_CATEGORIES] = g_param_spec_string( + "categories", + "Categories", + "Semicolon-separated list of application categories.", + NULL, + G_PARAM_READABLE ); + app_props[APP_PROP_FREQUENCY] = g_param_spec_int64( + "frequency", + "Frequency", + "The number of times the application was opened.", + 0, + INT64_MAX, + 0, + G_PARAM_READABLE ); + app_props[APP_PROP_TITLE] = g_param_spec_override( + "title", + g_object_interface_find_property( launcher_item_iface, "title" ) ); + app_props[APP_PROP_DESCRIPTION] = g_param_spec_override( + "description", + g_object_interface_find_property( launcher_item_iface, "description" ) ); + app_props[APP_PROP_ICON] = g_param_spec_override( + "icon", + g_object_interface_find_property( launcher_item_iface, "icon" ) ); + g_object_class_install_properties( object_klass, N_APP_PROPS, app_props ); +} + +// +// Static initialization of the FoobarLauncherItem interface. +// +void foobar_application_item_launcher_item_interface_init( FoobarLauncherItemInterface* iface ) +{ + iface->get_title = foobar_application_item_get_title; + iface->get_description = foobar_application_item_get_description; + iface->get_icon = foobar_application_item_get_icon; + iface->activate = foobar_application_item_activate; +} + +// +// Instance initialization for application items. +// +void foobar_application_item_init( FoobarApplicationItem* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_application_item_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarApplicationItem* self = (FoobarApplicationItem*)object; + + switch ( prop_id ) + { + case APP_PROP_ID: + g_value_set_string( value, foobar_application_item_get_id( self ) ); + break; + case APP_PROP_EXECUTABLE: + g_value_set_string( value, foobar_application_item_get_executable( self ) ); + break; + case APP_PROP_CATEGORIES: + g_value_set_string( value, foobar_application_item_get_categories( self ) ); + break; + case APP_PROP_FREQUENCY: + g_value_set_int64( value, foobar_application_item_get_frequency( self ) ); + break; + case APP_PROP_TITLE: + g_value_set_string( value, foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM ( self ) ) ); + break; + case APP_PROP_DESCRIPTION: + g_value_set_string( value, foobar_launcher_item_get_description( FOOBAR_LAUNCHER_ITEM( self ) ) ); + break; + case APP_PROP_ICON: + g_value_set_object( value, foobar_launcher_item_get_icon( FOOBAR_LAUNCHER_ITEM( self ) ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for application items. +// +void foobar_application_item_finalize( GObject* object ) +{ + FoobarApplicationItem* self = (FoobarApplicationItem*)object; + + g_clear_object( &self->info ); + + G_OBJECT_CLASS( foobar_application_item_parent_class )->finalize( object ); +} + +// +// Create a new application item belonging to the parent service (captured as an unowned reference). +// +FoobarApplicationItem* foobar_application_item_new( FoobarApplicationService* service ) +{ + FoobarApplicationItem* self = g_object_new( FOOBAR_TYPE_APPLICATION_ITEM, NULL ); + self->service = service; + return self; +} + +// +// Get the application's unique textual identifier. +// +gchar const* foobar_application_item_get_id( FoobarApplicationItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL ); + return self->info ? g_app_info_get_id( self->info ) : NULL; +} + +// +// Get the application's executable name. +// +gchar const* foobar_application_item_get_executable( FoobarApplicationItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL ); + return self->info ? g_app_info_get_executable( self->info ) : NULL; +} + +// +// Get the semicolon-separated list of categories for this application. +// +gchar const* foobar_application_item_get_categories( FoobarApplicationItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL ); + return G_IS_DESKTOP_APP_INFO( self->info ) + ? g_desktop_app_info_get_categories( G_DESKTOP_APP_INFO( self->info ) ) + : NULL; +} + +// +// Get the number of times this application was opened using the launcher. +// +gint64 foobar_application_item_get_frequency( FoobarApplicationItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), 0 ); + + if ( !self->service ) { return 0; } + gpointer value = g_hash_table_lookup( self->service->frequencies, foobar_application_item_get_id( self ) ); + return GPOINTER_TO_SIZE( value ); +} + +// +// Get the application's human-readable name. +// +gchar const* foobar_application_item_get_title( FoobarLauncherItem* item ) +{ + FoobarApplicationItem* self = ( FoobarApplicationItem* )item; + return self->info ? g_app_info_get_display_name( self->info ) : NULL; +} + +// +// Get an optional short description for the application. +// +gchar const* foobar_application_item_get_description( FoobarLauncherItem* item ) +{ + FoobarApplicationItem* self = ( FoobarApplicationItem* )item; + return self->info ? g_app_info_get_description( self->info ) : NULL; +} + +// +// Get an icon representing the application. +// +GIcon* foobar_application_item_get_icon( FoobarLauncherItem* item ) +{ + FoobarApplicationItem* self = ( FoobarApplicationItem* )item; + return self->info ? g_app_info_get_icon( self->info ) : NULL; +} + +// +// Launch the application, increasing its frequency by one. +// +void foobar_application_item_activate( FoobarLauncherItem* item ) +{ + FoobarApplicationItem* self = ( FoobarApplicationItem* )item; + + if ( !self->info ) { return; } + + g_autoptr( GError ) error = NULL; + if ( !g_app_info_launch( self->info, NULL, NULL, &error ) ) + { + g_warning( "Unable to launch application: %s", error->message ); + } + + gchar const* id = foobar_application_item_get_id( self ); + if ( !self->service || !id ) { return; } + + gpointer value = g_hash_table_lookup( self->service->frequencies, id ); + value = GSIZE_TO_POINTER( GPOINTER_TO_SIZE( value ) + 1 ); + g_mutex_lock( &self->service->frequencies_mutex ); + g_hash_table_insert( self->service->frequencies, g_strdup( id ), value ); + g_mutex_unlock( &self->service->frequencies_mutex ); + + g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_FREQUENCY] ); + gtk_sorter_changed( gtk_sort_list_model_get_sorter( self->service->sorted_items ), GTK_SORTER_CHANGE_DIFFERENT ); + foobar_application_service_write_cache( self->service ); +} + +// +// Update the backing GAppInfo object for the application. +// +void foobar_application_item_set_info( + FoobarApplicationItem* self, + GAppInfo* value ) +{ + g_return_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ) ); + + if ( self->info != value ) + { + g_clear_object( &self->info ); + + if ( value ) { self->info = g_object_ref( value ); } + + g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_ID] ); + g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_TITLE] ); + g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_DESCRIPTION] ); + g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_EXECUTABLE] ); + g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_ICON] ); + g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_FREQUENCY] ); + } +} + +// +// Match the item against the given search terms. +// +gboolean foobar_application_item_match( + FoobarApplicationItem* self, + gchar const* const* terms ) +{ + g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), FALSE ); + g_return_val_if_fail( terms != NULL, FALSE ); + + for ( gchar const* const* it = terms; *it; ++it ) + { + gchar const* term = *it; + gchar const* name = foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM( self ) ); + gchar const* description = foobar_launcher_item_get_description( FOOBAR_LAUNCHER_ITEM( self ) ); + gchar const* executable = foobar_application_item_get_executable( self ); + gchar const* categories = foobar_application_item_get_categories( self ); + gchar const* id = foobar_application_item_get_id( self ); + if ( name && strcasestr( name, term ) != NULL ) { continue; } + if ( description && strcasestr( description, term ) != NULL ) { continue; } + if ( executable && strcasestr( executable, term ) != NULL ) { continue; } + if ( categories && strcasestr( categories, term ) != NULL ) { continue; } + if ( id && strcasestr( id, term ) != NULL ) { continue; } + + return FALSE; + } + + return TRUE; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the application service. +// +void foobar_application_service_class_init( FoobarApplicationServiceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_application_service_get_property; + object_klass->finalize = foobar_application_service_finalize; + + props[PROP_ITEMS] = g_param_spec_object( + "items", + "Items", + "Application items, sorted by their frequency.", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for the application service. +// +void foobar_application_service_init( FoobarApplicationService* self ) +{ + self->items = g_list_store_new( FOOBAR_TYPE_APPLICATION_ITEM ); + + GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_application_service_sort_func, NULL, NULL ); + self->sorted_items = gtk_sort_list_model_new( G_LIST_MODEL( g_object_ref( self->items ) ), GTK_SORTER( sorter ) ); + + self->cache_path = foobar_get_cache_path( "application-frequencies.json" ); + self->frequencies = g_hash_table_new_full( g_str_hash, g_str_equal, g_free, NULL ); + g_mutex_init( &self->frequencies_mutex ); + g_mutex_init( &self->write_cache_mutex ); + + foobar_application_service_read_cache( self ); + foobar_application_service_update( self ); + + self->monitor = g_app_info_monitor_get( ); + self->changed_handler_id = g_signal_connect( + self->monitor, + "changed", + G_CALLBACK( foobar_application_service_handle_changed ), + self ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_application_service_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarApplicationService* self = (FoobarApplicationService*)object; + + switch ( prop_id ) + { + case PROP_ITEMS: + g_value_set_object( value, foobar_application_service_get_items( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for the application service. +// +void foobar_application_service_finalize( GObject* object ) +{ + FoobarApplicationService* self = (FoobarApplicationService*)object; + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->items ) ); ++i ) + { + FoobarApplicationItem* item = g_list_model_get_item( G_LIST_MODEL( self->items ), i ); + item->service = NULL; + g_object_notify_by_pspec( G_OBJECT( item ), app_props[APP_PROP_FREQUENCY] ); + } + g_clear_signal_handler( &self->changed_handler_id, self->monitor ); + g_clear_object( &self->sorted_items ); + g_clear_object( &self->items ); + g_clear_object( &self->monitor ); + g_clear_pointer( &self->frequencies, g_hash_table_unref ); + g_clear_pointer( &self->cache_path, g_free ); + + g_mutex_clear( &self->frequencies_mutex ); + g_mutex_clear( &self->write_cache_mutex ); + + G_OBJECT_CLASS( foobar_application_service_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new application service instance. +// +FoobarApplicationService* foobar_application_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_APPLICATION_SERVICE, NULL ); +} + +// +// Get a sorted list of all available applications. +// +GListModel* foobar_application_service_get_items( FoobarApplicationService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_APPLICATION_SERVICE( self ), NULL ); + + return G_LIST_MODEL( self->sorted_items ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called by GLib when the list of applications has changed. +// +void foobar_application_service_handle_changed( + GAppInfoMonitor* monitor, + gpointer userdata ) +{ + (void)monitor; + FoobarApplicationService* self = (FoobarApplicationService*)userdata; + + foobar_application_service_update( self ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Reload the list of applications. +// +void foobar_application_service_update( FoobarApplicationService* self ) +{ + g_list_store_remove_all( self->items ); + + g_autolist( GAppInfo ) info = g_app_info_get_all( ); + for ( GList* it = info; it; it = it->next ) + { + if ( g_app_info_should_show( it->data ) ) + { + g_autoptr( FoobarApplicationItem ) item = foobar_application_item_new( self ); + foobar_application_item_set_info( item, it->data ); + g_list_store_append( self->items, item ); + } + } +} + +// +// Synchronously read the frequency cache file at self->cache_path, populating self->frequencies. +// +void foobar_application_service_read_cache( FoobarApplicationService* self ) +{ + g_autoptr( GError ) error = NULL; + + if ( self->cache_path && g_file_test( self->cache_path, G_FILE_TEST_EXISTS ) ) + { + g_autoptr( JsonParser ) parser = json_parser_new( ); + if ( !json_parser_load_from_mapped_file( parser, self->cache_path, &error ) ) + { + g_warning( "Unable to read cached application frequencies: %s", error->message ); + return; + } + + JsonNode* root_node = json_parser_get_root( parser ); + JsonObject* root_object = json_node_get_object( root_node ); + json_object_foreach_member( root_object, foobar_application_service_read_cache_foreach_cb, self ); + } +} + +// +// Callback used by foobar_application_service_read_cache to read a single entry. +// +void foobar_application_service_read_cache_foreach_cb( + JsonObject* object, + gchar const* member_name, + JsonNode* member_node, + gpointer userdata ) +{ + (void)object; + FoobarApplicationService* self = (FoobarApplicationService*)userdata; + + gsize frequency = json_node_get_int( member_node ); + g_hash_table_insert( self->frequencies, g_strdup( member_name ), GSIZE_TO_POINTER( frequency ) ); +} + +// +// Asynchronously start writing the frequency cache file at self->cache_path. +// +void foobar_application_service_write_cache( FoobarApplicationService* self ) +{ + foobar_application_service_write_cache_async( self, NULL, foobar_application_service_write_cache_cb, NULL ); +} + +// +// Callback invoked when the frequency cache was written successfully or failed. +// +void foobar_application_service_write_cache_cb( + GObject* object, + GAsyncResult* result, + gpointer userdata ) +{ + (void)userdata; + FoobarApplicationService* self = (FoobarApplicationService*)object; + + g_autoptr( GError ) error = NULL; + if ( !foobar_application_service_write_cache_finish( self, result, &error ) ) + { + g_warning( "Unable to write cached application frequencies: %s", error->message ); + } +} + +// +// Asynchronously write the frequency cache file at self->cache_path. +// +void foobar_application_service_write_cache_async( + FoobarApplicationService* self, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer userdata ) +{ + g_autoptr( GTask ) task = g_task_new( self, cancellable, callback, userdata ); + g_task_set_name( task, "write-application-cache" ); + g_task_run_in_thread( task, foobar_application_service_write_cache_thread ); +} + +// +// Get the asynchronous result for writing the frequency cache file, returning TRUE on success, or FALSE on error. +// +gboolean foobar_application_service_write_cache_finish( + FoobarApplicationService* self, + GAsyncResult* result, + GError** error ) +{ + (void)self; + + return g_task_propagate_boolean( G_TASK( result ), error ); +} + +// +// Task implementation for foobar_application_service_write_cache_async, invoked on a background thread. +// +void foobar_application_service_write_cache_thread( + GTask* task, + gpointer source_object, + gpointer task_data, + GCancellable* cancellable ) +{ + (void)cancellable; + (void)task_data; + FoobarApplicationService* self = (FoobarApplicationService*)source_object; + + if ( !self->cache_path ) + { + g_task_return_boolean( task, TRUE ); + return; + } + + g_autoptr( GError ) error = NULL; + + g_autoptr( JsonBuilder ) builder = json_builder_new( ); + json_builder_begin_object( builder ); + + g_mutex_lock( &self->frequencies_mutex ); + g_hash_table_foreach( self->frequencies, foobar_application_service_write_cache_foreach_cb, builder ); + g_mutex_unlock( &self->frequencies_mutex ); + + json_builder_end_object( builder ); + + g_autoptr( JsonNode ) root_node = json_builder_get_root( builder ); + g_autoptr( JsonGenerator ) generator = json_generator_new( ); + json_generator_set_root( generator, root_node ); + + g_mutex_lock( &self->write_cache_mutex ); + gboolean success = json_generator_to_file( generator, self->cache_path, &error ); + g_mutex_unlock( &self->write_cache_mutex ); + if ( !success ) + { + g_task_return_error( task, g_steal_pointer( &error ) ); + return; + } + + g_task_return_boolean( task, TRUE ); +} + +// +// Callback used by foobar_application_service_write_cache_thread to build a single entry. +// +void foobar_application_service_write_cache_foreach_cb( + gpointer key, + gpointer value, + gpointer userdata ) +{ + JsonBuilder* builder = (JsonBuilder*)userdata; + + json_builder_set_member_name( builder, key ); + json_builder_add_int_value( builder, GPOINTER_TO_SIZE( value ) ); +} + +// +// Sorting callback for application items. Items are sorted based on: +// 1. frequency (descending) +// 2. name (ascending) +// 3. id (ascending) +// +gint foobar_application_service_sort_func( + gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ) +{ + (void)userdata; + + FoobarApplicationItem* app_a = (FoobarApplicationItem*)item_a; + FoobarApplicationItem* app_b = (FoobarApplicationItem*)item_b; + + gint64 freq_a = foobar_application_item_get_frequency( app_a ); + gint64 freq_b = foobar_application_item_get_frequency( app_b ); + if ( freq_a > freq_b ) { return -1; } + if ( freq_a < freq_b ) { return 1; } + + gchar const* name_a = foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM( app_a ) ); + gchar const* name_b = foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM( app_b ) ); + gint name_res = g_strcmp0( name_a, name_b ); + if ( name_res ) { return name_res; } + + gchar const* id_a = foobar_application_item_get_id( app_a ); + gchar const* id_b = foobar_application_item_get_id( app_b ); + return g_strcmp0( id_a, id_b ); +} diff --git a/csrc/services/application-service.h b/csrc/services/application-service.h new file mode 100644 index 0000000..dd043fc --- /dev/null +++ b/csrc/services/application-service.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_APPLICATION_ITEM foobar_application_item_get_type( ) +#define FOOBAR_TYPE_APPLICATION_SERVICE foobar_application_service_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarApplicationItem, foobar_application_item, FOOBAR, APPLICATION_ITEM, GObject ) + +gchar const* foobar_application_item_get_id ( FoobarApplicationItem* self ); +gchar const* foobar_application_item_get_executable ( FoobarApplicationItem* self ); +gchar const* foobar_application_item_get_categories ( FoobarApplicationItem* self ); +gint64 foobar_application_item_get_frequency ( FoobarApplicationItem* self ); +gboolean foobar_application_item_match ( FoobarApplicationItem* self, + gchar const* const* terms ); + +G_DECLARE_FINAL_TYPE( FoobarApplicationService, foobar_application_service, FOOBAR, APPLICATION_SERVICE, GObject ) + +FoobarApplicationService* foobar_application_service_new ( void ); +GListModel* foobar_application_service_get_items( FoobarApplicationService* self ); + +G_END_DECLS diff --git a/csrc/services/audio-service.c b/csrc/services/audio-service.c new file mode 100644 index 0000000..06345f2 --- /dev/null +++ b/csrc/services/audio-service.c @@ -0,0 +1,930 @@ +#include "services/audio-service.h" +#include +#include +#include +#include +#include +#include + +#define DEFAULT_VOLUME 25 + +// +// FoobarAudioDeviceKind: +// +// The type of an audio device (either speaker/output or microphone/input). +// + +G_DEFINE_ENUM_TYPE( + FoobarAudioDeviceKind, + foobar_audio_device_kind, + G_DEFINE_ENUM_VALUE( FOOBAR_AUDIO_DEVICE_INPUT, "input" ), + G_DEFINE_ENUM_VALUE( FOOBAR_AUDIO_DEVICE_OUTPUT, "output" ) ) + +// +// FoobarAudioDevice: +// +// Representation of a real input/output device or an abstract device (like the default device which only acts as a +// proxy for another device). +// + +struct _FoobarAudioDevice +{ + GObject parent_instance; + FoobarAudioService* service; + FoobarAudioDeviceKind kind; + GvcMixerStream* stream; + gboolean is_default; + gulong notify_handler_id; +}; + +enum +{ + DEVICE_PROP_KIND = 1, + DEVICE_PROP_ID, + DEVICE_PROP_NAME, + DEVICE_PROP_DESCRIPTION, + DEVICE_PROP_VOLUME, + DEVICE_PROP_IS_MUTED, + DEVICE_PROP_IS_DEFAULT, + DEVICE_PROP_IS_AVAILABLE, + N_DEVICE_PROPS, +}; + +static GParamSpec* device_props[N_DEVICE_PROPS] = { 0 }; + +static void foobar_audio_device_class_init ( FoobarAudioDeviceClass* klass ); +static void foobar_audio_device_init ( FoobarAudioDevice* self ); +static void foobar_audio_device_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_audio_device_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_audio_device_finalize ( GObject* object ); +static FoobarAudioDevice* foobar_audio_device_new ( FoobarAudioService* service, + FoobarAudioDeviceKind kind ); +static void foobar_audio_device_set_stream ( FoobarAudioDevice* self, + GvcMixerStream* value ); +static void foobar_audio_device_set_default ( FoobarAudioDevice* self, + gboolean value ); +static void foobar_audio_device_handle_notify( GObject* object, + GParamSpec* pspec, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarAudioDevice, foobar_audio_device, G_TYPE_OBJECT ) + +// +// FoobarAudioService: +// +// Service managing the available audio input/output devices. This is implemented using the Gnome Volume Control +// library. +// + +struct _FoobarAudioService +{ + GObject parent_instance; + GvcMixerControl* control; + GListStore* devices; + GtkSortListModel* sorted_devices; + GtkFilterListModel* inputs; + GtkFilterListModel* outputs; + FoobarAudioDevice* default_input; + FoobarAudioDevice* default_output; + gulong default_sink_handler_id; + gulong default_source_handler_id; + gulong stream_added_handler_id; + gulong stream_removed_handler_id; +}; + +enum +{ + PROP_INPUTS = 1, + PROP_OUTPUTS, + PROP_DEFAULT_INPUT, + PROP_DEFAULT_OUTPUT, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_audio_service_class_init ( FoobarAudioServiceClass* klass ); +static void foobar_audio_service_init ( FoobarAudioService* self ); +static void foobar_audio_service_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_audio_service_finalize ( GObject* object ); +static void foobar_audio_service_handle_default_sink_changed ( GvcMixerControl* control, + guint id, + gpointer userdata ); +static void foobar_audio_service_handle_default_source_changed( GvcMixerControl* control, + guint id, + gpointer userdata ); +static void foobar_audio_service_handle_stream_added ( GvcMixerControl* control, + guint id, + gpointer userdata ); +static void foobar_audio_service_handle_stream_removed ( GvcMixerControl* control, + guint id, + gpointer userdata ); +static gboolean foobar_audio_service_identify_stream ( GvcMixerStream* stream, + FoobarAudioDeviceKind* out_kind ); +static void foobar_audio_service_update_default ( GListModel* list, + guint id ); +static gint foobar_audio_service_sort_func ( gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ); +static gboolean foobar_audio_service_input_filter_func ( gpointer item, + gpointer userdata ); +static gboolean foobar_audio_service_output_filter_func ( gpointer item, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarAudioService, foobar_audio_service, G_TYPE_OBJECT ) + +// --------------------------------------------------------------------------------------------------------------------- +// Device +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for audio devices. +// +void foobar_audio_device_class_init( FoobarAudioDeviceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_audio_device_get_property; + object_klass->set_property = foobar_audio_device_set_property; + object_klass->finalize = foobar_audio_device_finalize; + + device_props[DEVICE_PROP_KIND] = g_param_spec_enum( + "kind", + "Kind", + "The kind of audio device.", + FOOBAR_TYPE_AUDIO_DEVICE_KIND, + FOOBAR_AUDIO_DEVICE_INPUT, + G_PARAM_READABLE ); + device_props[DEVICE_PROP_ID] = g_param_spec_uint( + "id", + "ID", + "Current numeric ID of the device (may change over time).", + 0, + UINT_MAX, + UINT_MAX, + G_PARAM_READABLE ); + device_props[DEVICE_PROP_NAME] = g_param_spec_string( + "name", + "Name", + "Current name of the device.", + NULL, + G_PARAM_READABLE ); + device_props[DEVICE_PROP_DESCRIPTION] = g_param_spec_string( + "description", + "Description", + "Short textual description of the device.", + NULL, + G_PARAM_READABLE ); + device_props[DEVICE_PROP_VOLUME] = g_param_spec_int( + "volume", + "Volume", + "Current volume in percent.", + 0, + 100, + 0, + G_PARAM_READWRITE ); + device_props[DEVICE_PROP_IS_MUTED] = g_param_spec_int( + "is-muted", + "Is Muted", + "Indicates whether the device is currently muted (i.e. the volume is 0).", + 0, + 100, + 0, + G_PARAM_READWRITE ); + device_props[DEVICE_PROP_IS_DEFAULT] = g_param_spec_boolean( + "is-default", + "Is Default", + "Indicates whether the device is currently also a default device (either microphone or speaker).", + FALSE, + G_PARAM_READABLE ); + device_props[DEVICE_PROP_IS_AVAILABLE] = g_param_spec_boolean( + "is-available", + "Is Available", + "Indicates whether the device is currently available to use/plugged in.", + FALSE, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_DEVICE_PROPS, device_props ); +} + +// +// Instance initialization for audio devices. +// +void foobar_audio_device_init( FoobarAudioDevice* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_audio_device_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarAudioDevice* self = (FoobarAudioDevice*)object; + + switch ( prop_id ) + { + case DEVICE_PROP_KIND: + g_value_set_enum( value, foobar_audio_device_get_kind( self ) ); + break; + case DEVICE_PROP_ID: + g_value_set_uint( value, foobar_audio_device_get_id( self ) ); + break; + case DEVICE_PROP_NAME: + g_value_set_string( value, foobar_audio_device_get_name( self ) ); + break; + case DEVICE_PROP_DESCRIPTION: + g_value_set_string( value, foobar_audio_device_get_description( self ) ); + break; + case DEVICE_PROP_VOLUME: + g_value_set_int( value, foobar_audio_device_get_volume( self ) ); + break; + case DEVICE_PROP_IS_MUTED: + g_value_set_boolean( value, foobar_audio_device_is_muted( self ) ); + break; + case DEVICE_PROP_IS_DEFAULT: + g_value_set_boolean( value, foobar_audio_device_is_default( self ) ); + break; + case DEVICE_PROP_IS_AVAILABLE: + g_value_set_boolean( value, foobar_audio_device_is_available( 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_audio_device_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarAudioDevice* self = (FoobarAudioDevice*)object; + + switch ( prop_id ) + { + case DEVICE_PROP_VOLUME: + foobar_audio_device_set_volume( self, g_value_get_int( value ) ); + break; + case DEVICE_PROP_IS_MUTED: + foobar_audio_device_set_muted( self, g_value_get_boolean( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for audio devices. +// +void foobar_audio_device_finalize( GObject* object ) +{ + FoobarAudioDevice* self = (FoobarAudioDevice*)object; + + g_clear_signal_handler( &self->notify_handler_id, self->stream ); + g_clear_object( &self->stream ); + + G_OBJECT_CLASS( foobar_audio_device_parent_class )->finalize( object ); +} + +// +// Create a new audio device object with the specified parent service (captured as an unowned reference). +// +FoobarAudioDevice* foobar_audio_device_new( + FoobarAudioService* service, + FoobarAudioDeviceKind kind ) +{ + FoobarAudioDevice* self = g_object_new( FOOBAR_TYPE_AUDIO_DEVICE, NULL ); + self->service = service; + self->kind = kind; + return self; +} + +// +// Get the type of this device. +// +FoobarAudioDeviceKind foobar_audio_device_get_kind( FoobarAudioDevice* self ) +{ + g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), 0 ); + return self->kind; +} + +// +// Get a numeric identifier for the device provided by the system. +// +guint foobar_audio_device_get_id( FoobarAudioDevice* self ) +{ + g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), 0 ); + return self->stream ? gvc_mixer_stream_get_id( self->stream ) : UINT_MAX; +} + +// +// Get the internal name for the device. This should not be used in the UI. +// +gchar const* foobar_audio_device_get_name( FoobarAudioDevice* self ) +{ + g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), NULL ); + return self->stream ? gvc_mixer_stream_get_name( self->stream ) : NULL; +} + +// +// Get a human-readable description for the device. +// +gchar const* foobar_audio_device_get_description( FoobarAudioDevice* self ) +{ + g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), NULL ); + return self->stream ? gvc_mixer_stream_get_description( self->stream ) : NULL; +} + +// +// Get the current volume as a percentage value. If muted, this is 0. +// +gint foobar_audio_device_get_volume( FoobarAudioDevice* self ) +{ + g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), 0 ); + + if ( !self->service ) { return 0; } + if ( self->stream && gvc_mixer_stream_get_is_muted( self->stream ) ) { return 0; } + gdouble max_volume = gvc_mixer_control_get_vol_max_norm( self->service->control ); + gdouble cur_volume = self->stream ? gvc_mixer_stream_get_volume( self->stream ) : 0; + gint percentage = (gint)round( 100. * cur_volume / max_volume ); + return CLAMP( percentage, 0, 100 ); +} + +// +// Check whether the device is currently muted, i.e. its volume is 0. +// +gboolean foobar_audio_device_is_muted( FoobarAudioDevice* self ) +{ + g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), FALSE ); + return foobar_audio_device_get_volume( self ) == 0; +} + +// +// Check whether this device is the system-default for its kind. +// +// This will always return TRUE for the static default device instance. +// +gboolean foobar_audio_device_is_default( FoobarAudioDevice* self ) +{ + g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), FALSE ); + return self->is_default; +} + +// +// Check whether the device is currently available. +// +gboolean foobar_audio_device_is_available( FoobarAudioDevice* self ) +{ + g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), FALSE ); + return self->stream != NULL; +} + +// +// Update the stream backing this device object. +// +void foobar_audio_device_set_stream( + FoobarAudioDevice* self, + GvcMixerStream* value ) +{ + g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) ); + + if ( self->stream != value ) + { + g_clear_signal_handler( &self->notify_handler_id, self->stream ); + g_clear_object( &self->stream ); + + if ( value ) + { + self->stream = g_object_ref( value ); + self->notify_handler_id = g_signal_connect( + self->stream, + "notify", + G_CALLBACK( foobar_audio_device_handle_notify ), + self ); + } + + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_ID] ); + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_NAME] ); + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_DESCRIPTION] ); + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_VOLUME] ); + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_MUTED] ); + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_AVAILABLE] ); + } +} + +// +// Update the device's "default" flag. This does not actually make it the default device. +// +void foobar_audio_device_set_default( + FoobarAudioDevice* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) ); + + value = !!value; + if ( self->is_default != value ) + { + self->is_default = value; + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_DEFAULT] ); + } +} + +// +// Update the device's volume as a percentage value. +// +void foobar_audio_device_set_volume( + FoobarAudioDevice* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) ); + + if ( !self->service || !self->stream ) { return; } + + value = CLAMP( value, 0, 100 ); + if ( foobar_audio_device_get_volume( self ) != value ) + { + gdouble max_volume = gvc_mixer_control_get_vol_max_norm( self->service->control ); + gdouble new_volume = value * max_volume / 100.; + gvc_mixer_stream_set_volume( self->stream, (guint32)new_volume ); + gvc_mixer_stream_push_volume( self->stream ); + gvc_mixer_stream_set_is_muted( self->stream, value == 0 ); + gvc_mixer_stream_change_is_muted( self->stream, value == 0 ); + } +} + +// +// Update the device's "muted" state. +// +// If the device was previously muted, the volume is set to either the previous volume or to a default value. +// +void foobar_audio_device_set_muted( + FoobarAudioDevice* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) ); + + if ( foobar_audio_device_is_muted( self ) != value ) + { + if ( self->stream ) + { + gvc_mixer_stream_set_is_muted( self->stream, value ); + gvc_mixer_stream_change_is_muted( self->stream, value ); + } + + if ( !value && foobar_audio_device_get_volume( self ) == 0 ) + { + foobar_audio_device_set_volume( self, DEFAULT_VOLUME ); + } + } +} + +// +// Let this device become default one for its kind. +// +void foobar_audio_device_make_default( FoobarAudioDevice* self ) +{ + g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) ); + + if ( !self->service || !self->stream ) { return; } + + if ( !foobar_audio_device_is_default( self ) ) + { + switch ( self->kind ) + { + case FOOBAR_AUDIO_DEVICE_INPUT: + gvc_mixer_control_set_default_source( self->service->control, self->stream ); + break; + case FOOBAR_AUDIO_DEVICE_OUTPUT: + gvc_mixer_control_set_default_sink( self->service->control, self->stream ); + break; + default: + break; + } + } +} + +// +// Called by the underlying stream when one of its properties changes. +// +void foobar_audio_device_handle_notify( + GObject* object, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)object; + FoobarAudioDevice* self = (FoobarAudioDevice*)userdata; + + gchar const* property = g_param_spec_get_name( pspec ); + if ( !g_strcmp0( property, "id" ) ) + { + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_ID] ); + } + else if ( !g_strcmp0( property, "name" ) ) + { + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_NAME] ); + } + else if ( !g_strcmp0( property, "description" ) ) + { + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_DESCRIPTION] ); + } + else if ( !g_strcmp0( property, "volume" ) || !g_strcmp0( property, "is-muted" ) ) + { + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_VOLUME] ); + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_MUTED] ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the audio service. +// +void foobar_audio_service_class_init( FoobarAudioServiceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_audio_service_get_property; + object_klass->finalize = foobar_audio_service_finalize; + + props[PROP_INPUTS] = g_param_spec_object( + "inputs", + "Inputs", + "Sorted list of input devices.", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE ); + props[PROP_OUTPUTS] = g_param_spec_object( + "outputs", + "Outputs", + "Sorted list of output devices.", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE ); + props[PROP_DEFAULT_INPUT] = g_param_spec_object( + "default-input", + "Default Input", + "The default input device (reference remains constant).", + FOOBAR_TYPE_AUDIO_DEVICE, + G_PARAM_READABLE ); + props[PROP_DEFAULT_OUTPUT] = g_param_spec_object( + "default-output", + "Default Output", + "The default output device (reference remains constant).", + FOOBAR_TYPE_AUDIO_DEVICE, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for the audio service. +// +void foobar_audio_service_init( FoobarAudioService* self ) +{ + self->control = gvc_mixer_control_new( "Foobar mixer control" ); + self->devices = g_list_store_new( FOOBAR_TYPE_AUDIO_DEVICE ); + + GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_audio_service_sort_func, NULL, NULL ); + self->sorted_devices = gtk_sort_list_model_new( + G_LIST_MODEL( g_object_ref( self->devices ) ), + GTK_SORTER( sorter ) ); + + GtkCustomFilter* input_filter = gtk_custom_filter_new( foobar_audio_service_input_filter_func, NULL, NULL ); + self->inputs = gtk_filter_list_model_new( + G_LIST_MODEL( g_object_ref( self->sorted_devices ) ), + GTK_FILTER( input_filter ) ); + + GtkCustomFilter* output_filter = gtk_custom_filter_new( foobar_audio_service_output_filter_func, NULL, NULL ); + self->outputs = gtk_filter_list_model_new( + G_LIST_MODEL( g_object_ref( self->sorted_devices ) ), + GTK_FILTER( output_filter ) ); + + self->default_input = foobar_audio_device_new( self, FOOBAR_AUDIO_DEVICE_INPUT ); + self->default_output = foobar_audio_device_new( self, FOOBAR_AUDIO_DEVICE_OUTPUT ); + foobar_audio_device_set_default( self->default_input, TRUE ); + foobar_audio_device_set_default( self->default_output, TRUE ); + + self->default_sink_handler_id = g_signal_connect( + self->control, + "default-sink-changed", + G_CALLBACK( foobar_audio_service_handle_default_sink_changed ), + self ); + self->default_source_handler_id = g_signal_connect( + self->control, + "default-source-changed", + G_CALLBACK( foobar_audio_service_handle_default_source_changed ), + self ); + self->stream_added_handler_id = g_signal_connect( + self->control, + "stream-added", + G_CALLBACK( foobar_audio_service_handle_stream_added ), + self ); + self->stream_removed_handler_id = g_signal_connect( + self->control, + "stream-removed", + G_CALLBACK( foobar_audio_service_handle_stream_removed ), + self ); + + gvc_mixer_control_open( self->control ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_audio_service_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarAudioService* self = (FoobarAudioService*)object; + + switch ( prop_id ) + { + case PROP_INPUTS: + g_value_set_object( value, foobar_audio_service_get_inputs( self ) ); + break; + case PROP_OUTPUTS: + g_value_set_object( value, foobar_audio_service_get_outputs( self ) ); + break; + case PROP_DEFAULT_INPUT: + g_value_set_object( value, foobar_audio_service_get_default_input( self ) ); + break; + case PROP_DEFAULT_OUTPUT: + g_value_set_object( value, foobar_audio_service_get_default_output( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for the audio service. +// +void foobar_audio_service_finalize( GObject* object ) +{ + FoobarAudioService* self = (FoobarAudioService*)object; + + g_clear_signal_handler( &self->default_sink_handler_id, self->control ); + g_clear_signal_handler( &self->default_source_handler_id, self->control ); + g_clear_signal_handler( &self->stream_added_handler_id, self->control ); + g_clear_signal_handler( &self->stream_removed_handler_id, self->control ); + + gvc_mixer_control_close( self->control ); + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i ) + { + FoobarAudioDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i ); + device->service = NULL; + } + + self->default_input->service = NULL; + self->default_output->service = NULL; + + g_clear_object( &self->control ); + g_clear_object( &self->inputs ); + g_clear_object( &self->outputs ); + g_clear_object( &self->sorted_devices ); + g_clear_object( &self->devices ); + g_clear_object( &self->default_input ); + g_clear_object( &self->default_output ); + + G_OBJECT_CLASS( foobar_audio_service_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new audio service instance. +// +FoobarAudioService* foobar_audio_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_AUDIO_SERVICE, NULL ); +} + +// +// Get the default input device. This reference remains constant. +// +FoobarAudioDevice* foobar_audio_service_get_default_input( FoobarAudioService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL ); + return self->default_input; +} + +// +// Get the default output device. This reference remains constant. +// +FoobarAudioDevice* foobar_audio_service_get_default_output( FoobarAudioService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL ); + return self->default_output; +} + +// +// Get a sorted list of input devices. +// +GListModel* foobar_audio_service_get_inputs( FoobarAudioService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL ); + return G_LIST_MODEL( self->inputs ); +} + +// +// Get a sorted list of output devices. +// +GListModel* foobar_audio_service_get_outputs( FoobarAudioService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL ); + return G_LIST_MODEL( self->outputs ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called by the mixer control when the default output device has changed. +// +// This will update the "default" state of the device and set the backing stream for the static default device. +// +void foobar_audio_service_handle_default_sink_changed( + GvcMixerControl* control, + guint id, + gpointer userdata ) +{ + (void)control; + FoobarAudioService* self = (FoobarAudioService*)userdata; + + foobar_audio_service_update_default( G_LIST_MODEL( self->outputs ), id ); + + GvcMixerStream* stream = gvc_mixer_control_lookup_stream_id( self->control, id ); + foobar_audio_device_set_stream( self->default_output, stream ); +} + +// +// Called by the mixer control when the default input device has changed. +// +// This will update the "default" state of the device and set the backing stream for the static default device. +// +void foobar_audio_service_handle_default_source_changed( + GvcMixerControl* control, + guint id, + gpointer userdata ) +{ + (void)control; + FoobarAudioService* self = (FoobarAudioService*)userdata; + + foobar_audio_service_update_default( G_LIST_MODEL( self->inputs ), id ); + + GvcMixerStream* stream = gvc_mixer_control_lookup_stream_id( self->control, id ); + foobar_audio_device_set_stream( self->default_input, stream ); +} + +// +// Called when an audio device was added. +// +void foobar_audio_service_handle_stream_added( + GvcMixerControl* control, + guint id, + gpointer userdata ) +{ + (void)control; + FoobarAudioService* self = (FoobarAudioService*)userdata; + + GvcMixerStream* stream = gvc_mixer_control_lookup_stream_id( self->control, id ); + FoobarAudioDeviceKind kind; + if ( foobar_audio_service_identify_stream( stream, &kind ) ) + { + g_autoptr( FoobarAudioDevice ) device = foobar_audio_device_new( self, kind ); + foobar_audio_device_set_stream( device, stream ); + g_list_store_append( self->devices, device ); + } +} + +// +// Called when an audio device was removed. +// +void foobar_audio_service_handle_stream_removed( + GvcMixerControl* control, + guint id, + gpointer userdata ) +{ + (void)control; + FoobarAudioService* self = (FoobarAudioService*)userdata; + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i ) + { + FoobarAudioDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i ); + if ( foobar_audio_device_get_id( device ) == id ) + { + g_object_ref( device ); + device->service = NULL; + g_list_store_remove( self->devices, i ); + foobar_audio_device_set_stream( device, NULL ); + g_object_unref( device ); + break; + } + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Identify whether a stream is an input device or an output device, returning FALSE if unknown. +// +gboolean foobar_audio_service_identify_stream( + GvcMixerStream* stream, + FoobarAudioDeviceKind* out_kind ) +{ + if ( GVC_IS_MIXER_SINK( stream ) ) + { + *out_kind = FOOBAR_AUDIO_DEVICE_OUTPUT; + return TRUE; + } + + if ( GVC_IS_MIXER_SOURCE( stream ) ) + { + *out_kind = FOOBAR_AUDIO_DEVICE_INPUT; + return TRUE; + } + + return FALSE; +} + +// +// Set the "default" state for the device with the specified ID to TRUE and all other devices to FALSE. +// +void foobar_audio_service_update_default( + GListModel* list, + guint id ) +{ + for ( guint i = 0; i < g_list_model_get_n_items( list ); ++i ) + { + FoobarAudioDevice* device = g_list_model_get_item( list, i ); + gboolean is_default = foobar_audio_device_get_id( device ) == id; + foobar_audio_device_set_default( device, is_default ); + } +} + +// +// Sorting callback for audio devices. +// +gint foobar_audio_service_sort_func( + gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ) +{ + (void)userdata; + + FoobarAudioDevice* device_a = (FoobarAudioDevice*)item_a; + FoobarAudioDevice* device_b = (FoobarAudioDevice*)item_b; + return g_strcmp0( + foobar_audio_device_get_description( device_a ), + foobar_audio_device_get_description( device_b ) ); +} + +// +// Filtering callback for the list of input devices. +// +gboolean foobar_audio_service_input_filter_func( + gpointer item, + gpointer userdata ) +{ + (void)userdata; + + FoobarAudioDevice* device = item; + return foobar_audio_device_get_kind( device ) == FOOBAR_AUDIO_DEVICE_INPUT; +} + +// +// Filtering callback for the list of output devices. +// +gboolean foobar_audio_service_output_filter_func( + gpointer item, + gpointer userdata ) +{ + (void)userdata; + + FoobarAudioDevice* device = item; + return foobar_audio_device_get_kind( device ) == FOOBAR_AUDIO_DEVICE_OUTPUT; +} diff --git a/csrc/services/audio-service.h b/csrc/services/audio-service.h new file mode 100644 index 0000000..a833c20 --- /dev/null +++ b/csrc/services/audio-service.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_AUDIO_DEVICE_KIND foobar_audio_device_kind_get_type( ) +#define FOOBAR_TYPE_AUDIO_DEVICE foobar_audio_device_get_type( ) +#define FOOBAR_TYPE_AUDIO_SERVICE foobar_audio_service_get_type( ) + +typedef enum +{ + FOOBAR_AUDIO_DEVICE_INPUT = 0, + FOOBAR_AUDIO_DEVICE_OUTPUT, +} FoobarAudioDeviceKind; + +GType foobar_audio_device_kind_get_type( void ); + +G_DECLARE_FINAL_TYPE( FoobarAudioDevice, foobar_audio_device, FOOBAR, AUDIO_DEVICE, GObject ) + +FoobarAudioDeviceKind foobar_audio_device_get_kind ( FoobarAudioDevice* self ); +guint foobar_audio_device_get_id ( FoobarAudioDevice* self ); +gchar const* foobar_audio_device_get_name ( FoobarAudioDevice* self ); +gchar const* foobar_audio_device_get_description( FoobarAudioDevice* self ); +gint foobar_audio_device_get_volume ( FoobarAudioDevice* self ); +gboolean foobar_audio_device_is_muted ( FoobarAudioDevice* self ); +gboolean foobar_audio_device_is_default ( FoobarAudioDevice* self ); +gboolean foobar_audio_device_is_available ( FoobarAudioDevice* self ); +void foobar_audio_device_set_volume ( FoobarAudioDevice* self, + gint value ); +void foobar_audio_device_set_muted ( FoobarAudioDevice* self, + gboolean value ); +void foobar_audio_device_make_default ( FoobarAudioDevice* self ); + +G_DECLARE_FINAL_TYPE( FoobarAudioService, foobar_audio_service, FOOBAR, AUDIO_SERVICE, GObject ) + +FoobarAudioService* foobar_audio_service_new ( void ); +FoobarAudioDevice* foobar_audio_service_get_default_input ( FoobarAudioService* self ); +FoobarAudioDevice* foobar_audio_service_get_default_output( FoobarAudioService* self ); +GListModel* foobar_audio_service_get_inputs ( FoobarAudioService* self ); +GListModel* foobar_audio_service_get_outputs ( FoobarAudioService* self ); + +G_END_DECLS diff --git a/csrc/services/battery-service.c b/csrc/services/battery-service.c new file mode 100644 index 0000000..0457547 --- /dev/null +++ b/csrc/services/battery-service.c @@ -0,0 +1,378 @@ +#include "services/battery-service.h" +#include "dbus/upower.h" +#include + +#define DEVICE_STATE_CHARGING 1 +#define DEVICE_STATE_FULLY_CHARGED 4 + +// +// FoobarBatteryState: +// +// Structure storing the current information about the battery. +// + +struct _FoobarBatteryState +{ + gboolean is_charging; + gboolean is_charged; + gint percentage; + gint time_remaining; +}; + +G_DEFINE_BOXED_TYPE( FoobarBatteryState, foobar_battery_state, foobar_battery_state_copy, foobar_battery_state_free ) + +// +// FoobarBatteryService: +// +// Service monitoring the state of the battery. This is implemented using the UPower D-Bus API. +// + +struct _FoobarBatteryService +{ + GObject parent_instance; + FoobarBatteryState* state; + FoobarUPowerDevice* proxy; + gulong changed_handler_id; +}; + +enum +{ + PROP_STATE = 1, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_battery_service_class_init ( FoobarBatteryServiceClass* klass ); +static void foobar_battery_service_init ( FoobarBatteryService* self ); +static void foobar_battery_service_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_battery_service_finalize ( GObject* object ); +static void foobar_battery_service_handle_connect ( GObject* object, + GAsyncResult* result, + gpointer userdata ); +static void foobar_battery_service_handle_properties_changed( GDBusProxy* proxy, + GVariant* changed_properties, + GStrv invalidated_properties, + gpointer userdata ); +static void foobar_battery_service_update ( FoobarBatteryService* self ); + +G_DEFINE_FINAL_TYPE( FoobarBatteryService, foobar_battery_service, G_TYPE_OBJECT ) + +// --------------------------------------------------------------------------------------------------------------------- +// State +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new, default-initialized state structure. +// +FoobarBatteryState* foobar_battery_state_new( void ) +{ + return g_new0( FoobarBatteryState, 1 ); +} + +// +// Create a mutable copy of a state structure. +// +FoobarBatteryState* foobar_battery_state_copy( FoobarBatteryState const* self ) +{ + if ( !self ) { return NULL; } + + FoobarBatteryState* copy = g_new0( FoobarBatteryState, 1 ); + copy->is_charging = self->is_charging; + copy->is_charged = self->is_charged; + copy->percentage = self->percentage; + copy->time_remaining = self->time_remaining; + return copy; +} + +// +// Release resources associated with a state structure (NULL is allowed). +// +void foobar_battery_state_free( FoobarBatteryState* self ) +{ + g_free( self ); +} + +// +// Compare two battery state structures (NULL is allowed)/ +// +gboolean foobar_battery_state_equal( + FoobarBatteryState const* a, + FoobarBatteryState const* b ) +{ + if ( a == NULL && b == NULL ) { return TRUE; } + if ( a == NULL || b == NULL ) { return FALSE; } + + if ( a->is_charging != b->is_charging ) { return FALSE; } + if ( a->is_charged != b->is_charged ) { return FALSE; } + if ( a->percentage != b->percentage ) { return FALSE; } + if ( a->time_remaining != b->time_remaining ) { return FALSE; } + + return TRUE; +} + +// +// Indicates whether the battery is currently being charged. +// +gboolean foobar_battery_state_is_charging( FoobarBatteryState const* self ) +{ + g_return_val_if_fail( self != NULL, FALSE ); + return self->is_charging; +} + +// +// Indicates whether the battery is currently being charged. +// +void foobar_battery_state_set_charging( + FoobarBatteryState* self, + gboolean value ) +{ + g_return_if_fail( self != NULL ); + self->is_charging = !!value; +} + +// +// Indicates whether the battery is fully charged. +// +gboolean foobar_battery_state_is_charged( FoobarBatteryState const* self ) +{ + g_return_val_if_fail( self != NULL, FALSE ); + return self->is_charged; +} + +// +// Indicates whether the battery is fully charged. +// +void foobar_battery_state_set_charged( + FoobarBatteryState* self, + gboolean value ) +{ + g_return_if_fail( self != NULL ); + self->is_charged = !!value; +} + +// +// Current battery level as a percentage value. +// +gint foobar_battery_state_get_percentage( FoobarBatteryState const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->percentage; +} + +// +// Current battery level as a percentage value. +// +void foobar_battery_state_set_percentage( + FoobarBatteryState* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->percentage = CLAMP( value, 0, 100 ); +} + +// +// Estimated remaining time until fully charged or empty in seconds. +// +gint foobar_battery_state_get_time_remaining( FoobarBatteryState const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->time_remaining; +} + +// +// Estimated remaining time until fully charged or empty in seconds. +// +void foobar_battery_state_set_time_remaining( + FoobarBatteryState* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->time_remaining = MAX( value, 0 ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the battery service. +// +void foobar_battery_service_class_init( FoobarBatteryServiceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_battery_service_get_property; + object_klass->finalize = foobar_battery_service_finalize; + + props[PROP_STATE] = g_param_spec_boxed( + "state", + "State", + "Current state of the battery, if available.", + FOOBAR_TYPE_BATTERY_STATE, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for the battery service. +// +void foobar_battery_service_init( FoobarBatteryService* self ) +{ + foobar_upower_device_proxy_new_for_bus( + G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + "org.freedesktop.UPower", + "/org/freedesktop/UPower/devices/DisplayDevice", + NULL, + foobar_battery_service_handle_connect, + g_object_ref( self ) ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_battery_service_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarBatteryService* self = (FoobarBatteryService*)object; + + switch ( prop_id ) + { + case PROP_STATE: + g_value_set_boxed( value, foobar_battery_service_get_state( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for the battery service. +// +void foobar_battery_service_finalize( GObject* object ) +{ + FoobarBatteryService* self = (FoobarBatteryService*)object; + + g_clear_signal_handler( &self->changed_handler_id, self->proxy ); + g_clear_object( &self->proxy ); + g_clear_pointer( &self->state, foobar_battery_state_free ); + + G_OBJECT_CLASS( foobar_battery_service_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new battery service instance. +// +FoobarBatteryService* foobar_battery_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_BATTERY_SERVICE, NULL ); +} + +// +// Get the current state of the battery, if available. +// +FoobarBatteryState const* foobar_battery_service_get_state( FoobarBatteryService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_BATTERY_SERVICE( self ), NULL ); + + return self->state; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called after asynchronous initialization of the D-Bus proxy. +// +void foobar_battery_service_handle_connect( + GObject* object, + GAsyncResult* result, + gpointer userdata ) +{ + (void)object; + g_autoptr( FoobarBatteryService ) self = (FoobarBatteryService*)userdata; + + g_autoptr( GError ) error = NULL; + self->proxy = foobar_upower_device_proxy_new_for_bus_finish( result, &error ); + if ( self->proxy ) + { + self->changed_handler_id = g_signal_connect( + self->proxy, + "g-properties-changed", + G_CALLBACK( foobar_battery_service_handle_properties_changed ), + self ); + foobar_battery_service_update( self ); + } + else + { + g_warning( "Unable to connect to battery status service: %s", error->message ); + } +} + +// +// Called by the D-Bus server when the battery state has changed. +// +void foobar_battery_service_handle_properties_changed( + GDBusProxy* proxy, + GVariant* changed_properties, + GStrv invalidated_properties, + gpointer userdata ) +{ + (void)proxy; + (void)changed_properties; + (void)invalidated_properties; + FoobarBatteryService* self = (FoobarBatteryService*)userdata; + + foobar_battery_service_update( self ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Reload the battery state. +// +void foobar_battery_service_update( FoobarBatteryService* self ) +{ + g_return_if_fail( FOOBAR_IS_BATTERY_SERVICE( self ) ); + + FoobarBatteryState* new_state = NULL; + if ( self->proxy && foobar_upower_device_get_is_present( self->proxy ) ) + { + guint state = foobar_upower_device_get_state( self->proxy ); + gint percent = (gint)round( foobar_upower_device_get_percentage( self->proxy ) ); + gboolean is_charging = state == DEVICE_STATE_CHARGING; + gboolean is_charged = + state == DEVICE_STATE_FULLY_CHARGED || + ( state == DEVICE_STATE_CHARGING && percent == 100 ); + gint time_remaining = is_charging + ? (gint)foobar_upower_device_get_time_to_full( self->proxy ) + : (gint)foobar_upower_device_get_time_to_empty( self->proxy ); + + new_state = foobar_battery_state_new( ); + foobar_battery_state_set_charging( new_state, is_charging ); + foobar_battery_state_set_charged( new_state, is_charged ); + foobar_battery_state_set_percentage( new_state, percent ); + foobar_battery_state_set_time_remaining( new_state, time_remaining ); + } + + if ( !foobar_battery_state_equal( self->state, new_state ) ) + { + g_clear_pointer( &self->state, foobar_battery_state_free ); + self->state = new_state; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_STATE] ); + } +} diff --git a/csrc/services/battery-service.h b/csrc/services/battery-service.h new file mode 100644 index 0000000..bf15c4a --- /dev/null +++ b/csrc/services/battery-service.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_BATTERY_STATE foobar_battery_state_get_type( ) +#define FOOBAR_TYPE_BATTERY_SERVICE foobar_battery_service_get_type( ) + +typedef struct _FoobarBatteryState FoobarBatteryState; + +GType foobar_battery_state_get_type ( void ); +FoobarBatteryState* foobar_battery_state_new ( void ); +FoobarBatteryState* foobar_battery_state_copy ( FoobarBatteryState const* self ); +void foobar_battery_state_free ( FoobarBatteryState* self ); +gboolean foobar_battery_state_equal ( FoobarBatteryState const* a, + FoobarBatteryState const* b ); +gboolean foobar_battery_state_is_charging ( FoobarBatteryState const* self ); +gboolean foobar_battery_state_is_charged ( FoobarBatteryState const* self ); +gint foobar_battery_state_get_percentage ( FoobarBatteryState const* self ); +gint foobar_battery_state_get_time_remaining( FoobarBatteryState const* self ); +void foobar_battery_state_set_charging ( FoobarBatteryState* self, + gboolean value ); +void foobar_battery_state_set_charged ( FoobarBatteryState* self, + gboolean value ); +void foobar_battery_state_set_percentage ( FoobarBatteryState* self, + gint value ); +void foobar_battery_state_set_time_remaining( FoobarBatteryState* self, + gint value ); + +G_DECLARE_FINAL_TYPE( FoobarBatteryService, foobar_battery_service, FOOBAR, BATTERY_SERVICE, GObject ) + +FoobarBatteryService* foobar_battery_service_new ( void ); +FoobarBatteryState const* foobar_battery_service_get_state( FoobarBatteryService* self ); + +G_END_DECLS diff --git a/csrc/services/bluetooth-service.c b/csrc/services/bluetooth-service.c new file mode 100644 index 0000000..eb13968 --- /dev/null +++ b/csrc/services/bluetooth-service.c @@ -0,0 +1,949 @@ +#include "services/bluetooth-service.h" +#include "dbus/bluez.h" +#include + +// +// FoobarBluetoothDeviceState: +// +// The current connection state of a remote bluetooth device. +// + +G_DEFINE_ENUM_TYPE( + FoobarBluetoothDeviceState, + foobar_bluetooth_device_state, + G_DEFINE_ENUM_VALUE( FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED, "disconnected" ), + G_DEFINE_ENUM_VALUE( FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING, "connecting" ), + G_DEFINE_ENUM_VALUE( FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED, "connected" ) ) + +// +// FoobarBluetoothDevice: +// +// Represents an external bluetooth device the client can connect to. +// + +struct _FoobarBluetoothDevice +{ + GObject parent_instance; + FoobarBluetoothService* service; + FoobarBluetoothDeviceState state; + FoobarBluezDevice* device; + GCancellable* connect_cancellable; + gulong notify_handler_id; +}; + +enum +{ + DEVICE_PROP_NAME = 1, + DEVICE_PROP_STATE, + N_DEVICE_PROPS, +}; + +static GParamSpec* device_props[N_DEVICE_PROPS] = { 0 }; + +static void foobar_bluetooth_device_class_init ( FoobarBluetoothDeviceClass* klass ); +static void foobar_bluetooth_device_init ( FoobarBluetoothDevice* self ); +static void foobar_bluetooth_device_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_bluetooth_device_finalize ( GObject* object ); +static FoobarBluetoothDevice* foobar_bluetooth_device_new ( FoobarBluetoothService* service, + FoobarBluezDevice* device ); +static void foobar_bluetooth_device_handle_notify( GObject* object, + GParamSpec* pspec, + gpointer userdata ); +static void foobar_bluetooth_device_connect_cb ( GObject* object, + GAsyncResult* result, + gpointer userdata ); +static void foobar_bluetooth_device_disconnect_cb( GObject* object, + GAsyncResult* result, + gpointer userdata ); +static void foobar_bluetooth_device_update_state ( FoobarBluetoothDevice* self ); + +G_DEFINE_FINAL_TYPE( FoobarBluetoothDevice, foobar_bluetooth_device, G_TYPE_OBJECT ) + +// +// FoobarBluetoothService: +// +// Service managing the state of the bluetooth adapter. This is implemented using the Bluez DBus API. +// + +struct _FoobarBluetoothService +{ + GObject parent_instance; + GDBusObjectManager* object_manager; + GListStore* devices; + GtkFilterListModel* filtered_devices; + GtkSortListModel* sorted_devices; + GtkFilterListModel* connected_devices; + GPtrArray* adapters; // First adapter is the default one. + FoobarBluezAdapter* default_adapter; + gulong interface_added_handler_id; + gulong interface_removed_handler_id; + gulong object_added_handler_id; + gulong object_removed_handler_id; + gulong adapter_notify_handler_id; +}; + +enum +{ + PROP_DEVICES = 1, + PROP_CONNECTED_DEVICES, + PROP_IS_AVAILABLE, + PROP_IS_ENABLED, + PROP_IS_SCANNING, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_bluetooth_service_class_init ( FoobarBluetoothServiceClass* klass ); +static void foobar_bluetooth_service_init ( FoobarBluetoothService* self ); +static void foobar_bluetooth_service_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_bluetooth_service_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_bluetooth_service_finalize ( GObject* object ); +static void foobar_bluetooth_service_handle_interface_added ( GDBusObjectManager* manager, + GDBusObject* object, + GDBusInterface* interface, + gpointer userdata ); +static void foobar_bluetooth_service_handle_interface_removed( GDBusObjectManager* manager, + GDBusObject* object, + GDBusInterface* interface, + gpointer userdata ); +static void foobar_bluetooth_service_handle_object_added ( GDBusObjectManager* manager, + GDBusObject* object, + gpointer userdata ); +static void foobar_bluetooth_service_handle_object_removed ( GDBusObjectManager* manager, + GDBusObject* object, + gpointer userdata ); +static void foobar_bluetooth_service_handle_adapter_notify ( GObject* object, + GParamSpec* pspec, + gpointer userdata ); +static void foobar_bluetooth_service_update_default_adapter ( FoobarBluetoothService* self ); +static GType foobar_bluetooth_service_dbus_proxy_type_cb ( GDBusObjectManagerClient* manager, + gchar const* object_path, + gchar const* interface_name, + gpointer userdata ); +static gboolean foobar_bluetooth_service_filter_func ( gpointer item, + gpointer userdata ); +static gboolean foobar_bluetooth_service_connected_filter_func ( gpointer item, + gpointer userdata ); +static gint foobar_bluetooth_service_sort_func ( gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarBluetoothService, foobar_bluetooth_service, G_TYPE_OBJECT ) + +// --------------------------------------------------------------------------------------------------------------------- +// Device +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for devices. +// +void foobar_bluetooth_device_class_init( FoobarBluetoothDeviceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_bluetooth_device_get_property; + object_klass->finalize = foobar_bluetooth_device_finalize; + + device_props[DEVICE_PROP_NAME] = g_param_spec_string( + "name", + "Name", + "Human-readable name for the device.", + NULL, + G_PARAM_READABLE ); + device_props[DEVICE_PROP_STATE] = g_param_spec_enum( + "state", + "State", + "Current connection state of the device.", + FOOBAR_TYPE_BLUETOOTH_DEVICE_STATE, + 0, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_DEVICE_PROPS, device_props ); +} + +// +// Instance initialization for devices. +// +void foobar_bluetooth_device_init( FoobarBluetoothDevice* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_bluetooth_device_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarBluetoothDevice* self = (FoobarBluetoothDevice*)object; + + switch ( prop_id ) + { + case DEVICE_PROP_NAME: + g_value_set_string( value, foobar_bluetooth_device_get_name( self ) ); + break; + case DEVICE_PROP_STATE: + g_value_set_enum( value, foobar_bluetooth_device_get_state( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for devices. +// +void foobar_bluetooth_device_finalize( GObject* object ) +{ + FoobarBluetoothDevice* self = (FoobarBluetoothDevice*)object; + + g_clear_signal_handler( &self->notify_handler_id, self->device ); + g_clear_object( &self->device ); + g_clear_object( &self->connect_cancellable ); + + G_OBJECT_CLASS( foobar_bluetooth_device_parent_class )->finalize( object ); +} + +// +// Create a new device object, wrapping a DBus device instance. +// +FoobarBluetoothDevice* foobar_bluetooth_device_new( + FoobarBluetoothService* service, + FoobarBluezDevice* device ) +{ + FoobarBluetoothDevice* self = g_object_new( FOOBAR_TYPE_BLUETOOTH_DEVICE, NULL ); + + self->service = service; + self->device = g_object_ref( device ); + foobar_bluetooth_device_update_state( self ); + + self->notify_handler_id = g_signal_connect( + self->device, "notify", G_CALLBACK( foobar_bluetooth_device_handle_notify ), self ); + + return self; +} + +// +// Get the name/alias for the device. +// +gchar const* foobar_bluetooth_device_get_name( FoobarBluetoothDevice* self ) +{ + g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_DEVICE( self ), NULL ); + return foobar_bluez_device_get_alias( self->device ); +} + +// +// Get the current connection state for the device. +// +FoobarBluetoothDeviceState foobar_bluetooth_device_get_state( FoobarBluetoothDevice* self ) +{ + g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_DEVICE( self ), 0 ); + return self->state; +} + +// +// Toggle whether the device is connected: +// +// - If the device is currently disconnected, it is connected. +// - If the device is currently being connected, that process is aborted. +// - If the device is currently connected, it is disconnected. +// +void foobar_bluetooth_device_toggle_connection( FoobarBluetoothDevice* self ) +{ + g_return_if_fail( FOOBAR_IS_BLUETOOTH_DEVICE( self ) ); + + switch ( self->state ) + { + case FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED: + self->connect_cancellable = g_cancellable_new( ); + foobar_bluetooth_device_update_state( self ); + foobar_bluez_device_call_connect( + self->device, + self->connect_cancellable, + foobar_bluetooth_device_connect_cb, + g_object_ref( self ) ); + break; + case FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING: + g_cancellable_cancel( self->connect_cancellable ); + break; + case FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED: + foobar_bluez_device_call_disconnect( + self->device, + NULL, + foobar_bluetooth_device_disconnect_cb, + g_object_ref( self ) ); + break; + default: + g_warn_if_reached( ); + } +} + +// +// Called by the underlying device when one of its properties changes. +// +void foobar_bluetooth_device_handle_notify( + GObject* object, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)object; + FoobarBluetoothDevice* self = (FoobarBluetoothDevice*)userdata; + + gchar const* property = g_param_spec_get_name( pspec ); + if ( !g_strcmp0( property, "alias" ) ) + { + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_NAME] ); + } + else if ( !g_strcmp0( property, "name" ) || !g_strcmp0( property, "adapter" ) ) + { + if ( self->service ) + { + gtk_filter_changed( + gtk_filter_list_model_get_filter( self->service->filtered_devices ), + GTK_FILTER_CHANGE_DIFFERENT ); + } + } + else if ( !g_strcmp0( property, "connected" ) ) + { + foobar_bluetooth_device_update_state( self ); + } +} + +// +// Called when a request to connect the device is completed. +// +// This is mainly used to update the device's state. +// +void foobar_bluetooth_device_connect_cb( + GObject* object, + GAsyncResult* result, + gpointer userdata ) +{ + (void)object; + g_autoptr( FoobarBluetoothDevice ) self = (FoobarBluetoothDevice*)userdata; + + g_autoptr( GError ) error = NULL; + if ( !foobar_bluez_device_call_connect_finish( self->device, result, &error ) ) + { + g_warning( "Unable to connect bluetooth device: %s", error->message ); + } + + g_clear_object( &self->connect_cancellable ); + foobar_bluetooth_device_update_state( self ); +} + +// +// Called when a request to disconnect the device is completed. +// +void foobar_bluetooth_device_disconnect_cb( + GObject* object, + GAsyncResult* result, + gpointer userdata ) +{ + (void)object; + g_autoptr( FoobarBluetoothDevice ) self = (FoobarBluetoothDevice*)userdata; + + g_autoptr( GError ) error = NULL; + if ( !foobar_bluez_device_call_disconnect_finish( self->device, result, &error ) ) + { + g_warning( "Unable to disconnect bluetooth device: %s", error->message ); + } +} + +// +// Update the current device state. +// +// The connect_cancellable member variable is used to indicate an ongoing connection attempt. Otherwise, the value of +// is_connected is used. +// +void foobar_bluetooth_device_update_state( FoobarBluetoothDevice* self ) +{ + FoobarBluetoothDeviceState new_state = foobar_bluez_device_get_connected( self->device ) + ? FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED + : FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED; + if ( self->connect_cancellable ) + { + new_state = FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING; + } + + if ( self->state != new_state ) + { + self->state = new_state; + g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_STATE] ); + + if ( self->service ) + { + gtk_filter_changed( + gtk_filter_list_model_get_filter( self->service->connected_devices ), + GTK_FILTER_CHANGE_DIFFERENT ); + } + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the bluetooth service. +// +void foobar_bluetooth_service_class_init( FoobarBluetoothServiceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_bluetooth_service_get_property; + object_klass->set_property = foobar_bluetooth_service_set_property; + object_klass->finalize = foobar_bluetooth_service_finalize; + + props[PROP_DEVICES] = g_param_spec_object( + "devices", + "Devices", + "Sorted list of available bluetooth devices.", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE ); + props[PROP_CONNECTED_DEVICES] = g_param_spec_object( + "connected-devices", + "Connected Devices", + "Sorted list of currently connected bluetooth devices.", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE ); + props[PROP_IS_AVAILABLE] = g_param_spec_boolean( + "is-available", + "Is Available", + "Indicates whether the system has a bluetooth adapter.", + FALSE, + G_PARAM_READABLE ); + props[PROP_IS_ENABLED] = g_param_spec_boolean( + "is-enabled", + "Is Enabled", + "Indicates whether the bluetooth adapter is currently enabled.", + FALSE, + G_PARAM_READWRITE ); + props[PROP_IS_SCANNING] = g_param_spec_boolean( + "is-scanning", + "Is Scanning", + "Indicates whether the bluetooth adapter is currently scanning for devices.", + FALSE, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for the bluetooth service. +// +void foobar_bluetooth_service_init( FoobarBluetoothService* self ) +{ + self->devices = g_list_store_new( FOOBAR_TYPE_BLUETOOTH_DEVICE ); + + GtkCustomFilter* filter = gtk_custom_filter_new( foobar_bluetooth_service_filter_func, self, NULL ); + self->filtered_devices = gtk_filter_list_model_new( + G_LIST_MODEL( g_object_ref( self->devices ) ), + GTK_FILTER( filter ) ); + + GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_bluetooth_service_sort_func, NULL, NULL ); + self->sorted_devices = gtk_sort_list_model_new( + G_LIST_MODEL( g_object_ref( self->filtered_devices ) ), + GTK_SORTER( sorter ) ); + + GtkCustomFilter* connected_filter = gtk_custom_filter_new( foobar_bluetooth_service_connected_filter_func, NULL, NULL ); + self->connected_devices = gtk_filter_list_model_new( + G_LIST_MODEL( g_object_ref( self->sorted_devices ) ), + GTK_FILTER( connected_filter ) ); + + self->adapters = g_ptr_array_new_with_free_func( g_object_unref ); + + g_autoptr( GError ) error = NULL; + self->object_manager = g_dbus_object_manager_client_new_for_bus_sync( + G_BUS_TYPE_SYSTEM, + G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START, + "org.bluez", + "/", + foobar_bluetooth_service_dbus_proxy_type_cb, + NULL, + NULL, + NULL, + &error ); + + if ( self->object_manager ) + { + self->interface_added_handler_id = g_signal_connect( + self->object_manager, + "interface-added", + G_CALLBACK( foobar_bluetooth_service_handle_interface_added ), + self ); + self->interface_removed_handler_id = g_signal_connect( + self->object_manager, + "interface-removed", + G_CALLBACK( foobar_bluetooth_service_handle_interface_removed ), + self ); + self->object_added_handler_id = g_signal_connect( + self->object_manager, + "object-added", + G_CALLBACK( foobar_bluetooth_service_handle_object_added ), + self ); + self->object_removed_handler_id = g_signal_connect( + self->object_manager, + "object-removed", + G_CALLBACK( foobar_bluetooth_service_handle_object_removed ), + self ); + + g_autolist( GDBusObject ) objects = g_dbus_object_manager_get_objects( self->object_manager ); + for ( GList* it = objects; it; it = it->next ) + { + foobar_bluetooth_service_handle_object_added( self->object_manager, it->data, self ); + } + } + else + { + g_warning( "Unable to create a DBus object manager for communication with Bluez: %s", error->message ); + } +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_bluetooth_service_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarBluetoothService* self = (FoobarBluetoothService*)object; + + switch ( prop_id ) + { + case PROP_DEVICES: + g_value_set_object( value, foobar_bluetooth_service_get_devices( self ) ); + break; + case PROP_CONNECTED_DEVICES: + g_value_set_object( value, foobar_bluetooth_service_get_connected_devices( self ) ); + break; + case PROP_IS_AVAILABLE: + g_value_set_boolean( value, foobar_bluetooth_service_is_available( self ) ); + break; + case PROP_IS_ENABLED: + g_value_set_boolean( value, foobar_bluetooth_service_is_enabled( self ) ); + break; + case PROP_IS_SCANNING: + g_value_set_boolean( value, foobar_bluetooth_service_is_scanning( 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_bluetooth_service_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarBluetoothService* self = (FoobarBluetoothService*)object; + + switch ( prop_id ) + { + case PROP_IS_ENABLED: + foobar_bluetooth_service_set_enabled( self, g_value_get_boolean( value ) ); + break; + case PROP_IS_SCANNING: + foobar_bluetooth_service_set_scanning( self, g_value_get_boolean( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for the bluetooth service. +// +void foobar_bluetooth_service_finalize( GObject* object ) +{ + FoobarBluetoothService* self = (FoobarBluetoothService*)object; + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i ) + { + FoobarBluetoothDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i ); + device->service = NULL; + } + + g_clear_signal_handler( &self->adapter_notify_handler_id, self->default_adapter ); + g_clear_signal_handler( &self->interface_added_handler_id, self->object_manager ); + g_clear_signal_handler( &self->interface_removed_handler_id, self->object_manager ); + g_clear_signal_handler( &self->object_added_handler_id, self->object_manager ); + g_clear_signal_handler( &self->object_removed_handler_id, self->object_manager ); + g_clear_object( &self->object_manager ); + g_clear_object( &self->connected_devices ); + g_clear_object( &self->sorted_devices ); + g_clear_object( &self->filtered_devices ); + g_clear_object( &self->devices ); + g_clear_object( &self->default_adapter ); + g_clear_pointer( &self->adapters, g_ptr_array_unref ); + + G_OBJECT_CLASS( foobar_bluetooth_service_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new bluetooth service instance. +// +FoobarBluetoothService* foobar_bluetooth_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_BLUETOOTH_SERVICE, NULL ); +} + +// +// Get the sorted list of available bluetooth devices. +// +GListModel* foobar_bluetooth_service_get_devices( FoobarBluetoothService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), NULL ); + return G_LIST_MODEL( self->sorted_devices ); +} + +// +// Get the sorted list of currently connected bluetooth devices. +// +GListModel* foobar_bluetooth_service_get_connected_devices( FoobarBluetoothService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), NULL ); + return G_LIST_MODEL( self->connected_devices ); +} + +// +// Check whether the system has a bluetooth adapter. +// +gboolean foobar_bluetooth_service_is_available( FoobarBluetoothService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), FALSE ); + return self->default_adapter != NULL; +} + +// +// Check whether the bluetooth adapter is currently enabled. +// +gboolean foobar_bluetooth_service_is_enabled( FoobarBluetoothService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), FALSE ); + return self->default_adapter ? foobar_bluez_adapter_get_powered( self->default_adapter ) : FALSE; +} + +// +// Check whether the bluetooth adapter is currently scanning for devices. +// +gboolean foobar_bluetooth_service_is_scanning( FoobarBluetoothService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), FALSE ); + return self->default_adapter ? foobar_bluez_adapter_get_discovering( self->default_adapter ) : FALSE; +} + +// +// Update whether the bluetooth adapter is currently enabled. +// +void foobar_bluetooth_service_set_enabled( + FoobarBluetoothService* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ) ); + + if (self->default_adapter) + { + foobar_bluez_adapter_set_powered( self->default_adapter, value ); + } +} + +// +// Update whether the bluetooth adapter is currently scanning for devices. +// +void foobar_bluetooth_service_set_scanning( + FoobarBluetoothService* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ) ); + + g_autoptr( GError ) error = NULL; + + if (self->default_adapter && foobar_bluetooth_service_is_scanning( self ) != value) + { + if (value) + { + // Make this device discoverable while discovery is active. + + GVariantBuilder filter_builder; + g_variant_builder_init( &filter_builder, G_VARIANT_TYPE_VARDICT ); + g_variant_builder_add( &filter_builder, "{sv}", "Discoverable", g_variant_new_boolean( TRUE ) ); + GVariant* filter = g_variant_builder_end( &filter_builder ); + if ( !foobar_bluez_adapter_call_set_discovery_filter_sync( self->default_adapter, filter, NULL, &error ) ) + { + g_warning( "Failed to set discovery filter: %s", error->message ); + return; + } + if ( !foobar_bluez_adapter_call_start_discovery_sync( self->default_adapter, NULL, &error ) ) + { + g_warning( "Failed to start discovery: %s", error->message ); + } + } + else + { + if ( !foobar_bluez_adapter_call_stop_discovery_sync( self->default_adapter, NULL, &error ) ) + { + g_warning( "Failed to stop discovery: %s", error->message ); + } + } + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called when an interface was added to an existing DBus object (either adapter or device). +// +void foobar_bluetooth_service_handle_interface_added( + GDBusObjectManager* manager, + GDBusObject* object, + GDBusInterface* interface, + gpointer userdata ) +{ + (void)manager; + (void)object; + FoobarBluetoothService* self = (FoobarBluetoothService*)userdata; + + if ( FOOBAR_IS_BLUEZ_DEVICE( interface ) ) + { + g_autoptr( FoobarBluetoothDevice ) device = foobar_bluetooth_device_new( + self, + FOOBAR_BLUEZ_DEVICE( interface ) ); + g_list_store_append( self->devices, device ); + } + else if ( FOOBAR_IS_BLUEZ_ADAPTER( interface ) ) + { + g_ptr_array_add( self->adapters, g_object_ref( interface ) ); + foobar_bluetooth_service_update_default_adapter( self ); + } +} + +// +// Called when an interface was removed from an existing DBus object (either adapter or device). +// +void foobar_bluetooth_service_handle_interface_removed( + GDBusObjectManager* manager, + GDBusObject* object, + GDBusInterface* interface, + gpointer userdata ) +{ + (void)manager; + FoobarBluetoothService* self = (FoobarBluetoothService*)userdata; + + gchar const* removed_path = g_dbus_object_get_object_path( object ); + if ( FOOBAR_IS_BLUEZ_DEVICE( interface ) ) + { + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i ) + { + FoobarBluetoothDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i ); + gchar const* path = g_dbus_proxy_get_object_path( G_DBUS_PROXY( device->device ) ); + if ( !g_strcmp0( path, removed_path ) ) + { + device->service = NULL; + g_list_store_remove( self->devices, i ); + break; + } + } + } + else if ( FOOBAR_IS_BLUEZ_ADAPTER( interface ) ) + { + for ( guint i = 0; i < self->adapters->len; ++i ) + { + FoobarBluezAdapter* adapter = g_ptr_array_index( self->adapters, i ); + gchar const* path = g_dbus_proxy_get_object_path( G_DBUS_PROXY( adapter ) ); + if ( !g_strcmp0( path, removed_path ) ) + { + g_ptr_array_remove_index( self->adapters, i ); + break; + } + } + foobar_bluetooth_service_update_default_adapter( self ); + } +} + +// +// Called when a DBus object was added (either adapter or device). +// +void foobar_bluetooth_service_handle_object_added( + GDBusObjectManager* manager, + GDBusObject* object, + gpointer userdata ) +{ + g_autolist( GDBusInterface ) interfaces = g_dbus_object_get_interfaces( object ); + for ( GList* it = interfaces; it; it = it->next ) + { + foobar_bluetooth_service_handle_interface_added( manager, object, it->data, userdata ); + } +} + +// +// Called when a DBus object was removed (either adapter or device). +// +void foobar_bluetooth_service_handle_object_removed( + GDBusObjectManager* manager, + GDBusObject* object, + gpointer userdata ) +{ + g_autolist( GDBusInterface ) interfaces = g_dbus_object_get_interfaces( object ); + for ( GList* it = interfaces; it; it = it->next ) + { + foobar_bluetooth_service_handle_interface_removed( manager, object, it->data, userdata ); + } +} + +// +// Called when a property of the default adapter has changed. +// +void foobar_bluetooth_service_handle_adapter_notify( + GObject* object, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)object; + FoobarBluetoothService* self = (FoobarBluetoothService*)userdata; + + gchar const* property = g_param_spec_get_name( pspec ); + if ( !g_strcmp0( property, "powered" ) ) + { + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_ENABLED] ); + } + else if ( !g_strcmp0( property, "discovering" ) ) + { + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_SCANNING] ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Update the default_adapter to match the first item in the adapters array. +// +void foobar_bluetooth_service_update_default_adapter( FoobarBluetoothService* self ) +{ + FoobarBluezAdapter* new_adapter = ( self->adapters->len > 0 ) ? g_ptr_array_index( self->adapters, 0 ) : NULL; + + if ( self->default_adapter != new_adapter ) + { + g_clear_signal_handler( &self->adapter_notify_handler_id, self->default_adapter ); + g_clear_object( &self->default_adapter ); + + if ( new_adapter ) + { + self->default_adapter = g_object_ref( new_adapter ); + self->adapter_notify_handler_id = g_signal_connect( + self->default_adapter, + "notify", + G_CALLBACK( foobar_bluetooth_service_handle_adapter_notify ), + self ); + } + + gtk_filter_changed( gtk_filter_list_model_get_filter( self->filtered_devices ), GTK_FILTER_CHANGE_DIFFERENT ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_AVAILABLE] ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_ENABLED] ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_SCANNING] ); + } +} + +// +// Get the type for creating a DBus proxy object. +// +// This is used because the DBus object manager should give us instances of the FoobarBluezAdapterProxy and +// FoobarBluezDeviceProxy types. +// +GType foobar_bluetooth_service_dbus_proxy_type_cb( + GDBusObjectManagerClient* manager, + gchar const* object_path, + gchar const* interface_name, + gpointer userdata ) +{ + if ( !interface_name ) + { + return G_TYPE_DBUS_OBJECT_PROXY; + } + + if ( !g_strcmp0( interface_name, "org.bluez.Device1" ) ) + { + return FOOBAR_TYPE_BLUEZ_DEVICE_PROXY; + } + + if ( !g_strcmp0( interface_name, "org.bluez.Adapter1" ) ) + { + return FOOBAR_TYPE_BLUEZ_ADAPTER_PROXY; + } + + return G_TYPE_DBUS_PROXY; +} + +// +// Filtering callback for the list of devices, only showing devices from the default adapter. +// +gboolean foobar_bluetooth_service_filter_func( + gpointer item, + gpointer userdata ) +{ + FoobarBluetoothService* self = (FoobarBluetoothService*)userdata; + FoobarBluetoothDevice* device = item; + + gchar const* device_adapter = foobar_bluez_device_get_adapter( device->device ); + gchar const* default_adapter = self->default_adapter + ? g_dbus_proxy_get_object_path( G_DBUS_PROXY( self->default_adapter ) ) + : NULL; + gchar const* name = foobar_bluez_device_get_name( device->device ); + return name != NULL && !g_strcmp0( device_adapter, default_adapter ); +} + +// +// Filtering callback for the list of connected devices. +// +gboolean foobar_bluetooth_service_connected_filter_func( + gpointer item, + gpointer userdata ) +{ + (void)userdata; + + FoobarBluetoothDevice* device = item; + return foobar_bluetooth_device_get_state( device ) == FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED; +} + +// +// Sorting callback for the list of devices. +// +// Devices are sorted by their names. +// +gint foobar_bluetooth_service_sort_func( + gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ) +{ + (void)userdata; + + FoobarBluetoothDevice* device_a = (FoobarBluetoothDevice*)item_a; + FoobarBluetoothDevice* device_b = (FoobarBluetoothDevice*)item_b; + return g_strcmp0( foobar_bluetooth_device_get_name( device_a ), foobar_bluetooth_device_get_name( device_b ) ); +} diff --git a/csrc/services/bluetooth-service.h b/csrc/services/bluetooth-service.h new file mode 100644 index 0000000..684fd45 --- /dev/null +++ b/csrc/services/bluetooth-service.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_BLUETOOTH_DEVICE_STATE foobar_bluetooth_device_state_get_type( ) +#define FOOBAR_TYPE_BLUETOOTH_DEVICE foobar_bluetooth_device_get_type( ) +#define FOOBAR_TYPE_BLUETOOTH_ADAPTER foobar_bluetooth_adapter_get_type( ) +#define FOOBAR_TYPE_BLUETOOTH_SERVICE foobar_bluetooth_service_get_type( ) + +typedef enum +{ + FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED, + FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING, + FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED, +} FoobarBluetoothDeviceState; + +GType foobar_bluetooth_device_state_get_type( void ); + +G_DECLARE_FINAL_TYPE( FoobarBluetoothDevice, foobar_bluetooth_device, FOOBAR, BLUETOOTH_DEVICE, GObject ) + +gchar const* foobar_bluetooth_device_get_name ( FoobarBluetoothDevice* self ); +FoobarBluetoothDeviceState foobar_bluetooth_device_get_state ( FoobarBluetoothDevice* self ); +void foobar_bluetooth_device_toggle_connection( FoobarBluetoothDevice* self ); + +G_DECLARE_FINAL_TYPE( FoobarBluetoothService, foobar_bluetooth_service, FOOBAR, BLUETOOTH_SERVICE, GObject ) + +FoobarBluetoothService* foobar_bluetooth_service_new ( void ); +GListModel* foobar_bluetooth_service_get_devices ( FoobarBluetoothService* self ); +GListModel* foobar_bluetooth_service_get_connected_devices( FoobarBluetoothService* self ); +gboolean foobar_bluetooth_service_is_available ( FoobarBluetoothService* self ); +gboolean foobar_bluetooth_service_is_enabled ( FoobarBluetoothService* self ); +gboolean foobar_bluetooth_service_is_scanning ( FoobarBluetoothService* self ); +void foobar_bluetooth_service_set_enabled ( FoobarBluetoothService* self, + gboolean value ); +void foobar_bluetooth_service_set_scanning ( FoobarBluetoothService* self, + gboolean value ); + +G_END_DECLS diff --git a/csrc/services/brightness-service.c b/csrc/services/brightness-service.c new file mode 100644 index 0000000..32f0501 --- /dev/null +++ b/csrc/services/brightness-service.c @@ -0,0 +1,391 @@ +#include "services/brightness-service.h" +#include +#include + +// +// FoobarBrightnessService: +// +// Service managing the brightness level. This is implemented by +// - for read access: monitoring the "brightness" file in a "/sys/class/backlight" subdirectory, +// - for write access: invoking the "brightnessctl" command. +// + +struct _FoobarBrightnessService +{ + GObject parent_instance; + gint percentage; + GFileMonitor* file_monitor; + gulong file_monitor_handler_id; + gchar* device_name; + gchar* file_path; + gint max_brightness; +}; + +enum +{ + PROP_PERCENTAGE = 1, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_brightness_service_class_init ( FoobarBrightnessServiceClass* klass ); +static void foobar_brightness_service_init ( FoobarBrightnessService* self ); +static void foobar_brightness_service_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_brightness_service_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_brightness_service_finalize ( GObject* object ); +static void foobar_brightness_service_handle_changed ( GFileMonitor* monitor, + GFile* file, + GFile* other_file, + GFileMonitorEvent event_type, + gpointer userdata ); +static gint foobar_brightness_service_load_percentage ( FoobarBrightnessService* self ); +static void foobar_brightness_service_write_percentage ( FoobarBrightnessService* self ); +static void foobar_brightness_service_write_percentage_watch_cb( GPid pid, + gint status, + gpointer userdata ); +static void foobar_brightness_service_get_info ( gchar** out_device_name, + gchar** out_file_path, + gint* out_max_brightness ); +static gint foobar_brightness_service_read_value ( gchar const* path ); +static GFileMonitor* foobar_brightness_service_monitor ( gchar const* path ); + +G_DEFINE_FINAL_TYPE( FoobarBrightnessService, foobar_brightness_service, G_TYPE_OBJECT ) + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the brightness service. +// +void foobar_brightness_service_class_init( FoobarBrightnessServiceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_brightness_service_get_property; + object_klass->set_property = foobar_brightness_service_set_property; + object_klass->finalize = foobar_brightness_service_finalize; + + props[PROP_PERCENTAGE] = g_param_spec_int( + "percentage", + "Percentage", + "Current brightness percentage.", + 0, + 100, + 100, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for the brightness service. +// +void foobar_brightness_service_init( FoobarBrightnessService* self ) +{ + foobar_brightness_service_get_info( &self->device_name, &self->file_path, &self->max_brightness ); + self->percentage = foobar_brightness_service_load_percentage( self ); + if ( self->file_path ) + { + self->file_monitor = foobar_brightness_service_monitor( self->file_path ); + } + if ( self->file_monitor ) + { + self->file_monitor_handler_id = g_signal_connect( + self->file_monitor, + "changed", + G_CALLBACK( foobar_brightness_service_handle_changed ), + self ); + } + else + { + self->file_monitor_handler_id = 0; + } +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_brightness_service_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarBrightnessService* self = (FoobarBrightnessService*)object; + + switch ( prop_id ) + { + case PROP_PERCENTAGE: + g_value_set_int( value, foobar_brightness_service_get_percentage( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Property setter implementation, mapping a property id to a method. +// +void foobar_brightness_service_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarBrightnessService* self = (FoobarBrightnessService*)object; + + switch ( prop_id ) + { + case PROP_PERCENTAGE: + foobar_brightness_service_set_percentage( self, g_value_get_int( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for the brightness service. +// +void foobar_brightness_service_finalize( GObject* object ) +{ + FoobarBrightnessService* self = (FoobarBrightnessService*)object; + + g_clear_signal_handler( &self->file_monitor_handler_id, self->file_monitor ); + g_clear_object( &self->file_monitor ); + g_clear_pointer( &self->device_name, g_free ); + g_clear_pointer( &self->file_path, g_free ); + + G_OBJECT_CLASS( foobar_brightness_service_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new brightness service instance. +// +FoobarBrightnessService* foobar_brightness_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_BRIGHTNESS_SERVICE, NULL ); +} + +// +// Get the current brightness percentage value. +// +gint foobar_brightness_service_get_percentage( FoobarBrightnessService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_BRIGHTNESS_SERVICE( self ), 0 ); + + return self->percentage; +} + +// +// Update the current brightness percentage value. +// +void foobar_brightness_service_set_percentage( + FoobarBrightnessService* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_BRIGHTNESS_SERVICE( self ) ); + + value = CLAMP( value, 0, 100 ); + if ( self->percentage != value ) + { + self->percentage = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_PERCENTAGE] ); + foobar_brightness_service_write_percentage( self ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called by the file monitor when the contents of the "brightness" file have changed. +// +void foobar_brightness_service_handle_changed( + GFileMonitor* monitor, + GFile* file, + GFile* other_file, + GFileMonitorEvent event_type, + gpointer userdata ) +{ + (void)monitor; + (void)file; + (void)other_file; + (void)event_type; + FoobarBrightnessService* self = (FoobarBrightnessService*)userdata; + + gint new_percentage = foobar_brightness_service_load_percentage( self ); + if ( self->percentage != new_percentage ) + { + self->percentage = new_percentage; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_PERCENTAGE] ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Load the current percentage value from the file at self->file_path. +// +// This requires file_path and max_brightness to be initialized, otherwise the brightness is assumed to be 100%. +// +gint foobar_brightness_service_load_percentage( FoobarBrightnessService* self ) +{ + if ( !self->file_path || self->max_brightness == -1 ) { return 100; } + + gint value = foobar_brightness_service_read_value( self->file_path ); + if ( value == -1 ) { return 100; } + + gdouble rel = value / (gdouble)self->max_brightness; + gint percentage = (gint)round( rel * 100. ); + return CLAMP( percentage, 0, 100 ); +} + +// +// Asynchronously update the current percentage value by invoking brightnessctl. +// +// This requires device_name to be initialized. +// +void foobar_brightness_service_write_percentage( FoobarBrightnessService* self ) +{ + if ( !self->device_name ) { return; } + + g_autofree gchar* percentage_string = g_strdup_printf( "%d%%", self->percentage ); + gchar* args[] = { "brightnessctl", "-d", self->device_name, "s", percentage_string, NULL }; + + g_autoptr( GError ) error = NULL; + GPid child_pid; + if ( !g_spawn_async( + NULL, + args, + NULL, + G_SPAWN_DO_NOT_REAP_CHILD + | G_SPAWN_SEARCH_PATH + | G_SPAWN_STDIN_FROM_DEV_NULL + | G_SPAWN_STDOUT_TO_DEV_NULL + | G_SPAWN_STDERR_TO_DEV_NULL, + NULL, + NULL, + &child_pid, + &error ) ) + { + g_warning( "Unable to spawn brightnessctl: %s", error->message ); + return; + } + + g_child_watch_add( child_pid, foobar_brightness_service_write_percentage_watch_cb, NULL ); +} + +// +// Called when the brightnessctl child process has finished. +// +void foobar_brightness_service_write_percentage_watch_cb( + GPid pid, + gint status, + gpointer userdata ) +{ + (void)userdata; + + g_autoptr( GError ) error = NULL; + if ( !g_spawn_check_wait_status( status, &error ) ) + { + g_warning( "Unable to set brightness with brightnessctl: %s", error->message ); + } + + g_spawn_close_pid( pid ); +} + +// +// Initialize information for the brightness device. +// +// This looks for the first directory in the /sys/class/backlight directories and uses it as the device name. It also +// populates out_file_path to the path of this directory and out_max_brightness to the contents of the corresponding +// max_brightness file. +// +void foobar_brightness_service_get_info( + gchar** out_device_name, + gchar** out_file_path, + gint* out_max_brightness ) +{ + *out_device_name = NULL; + *out_file_path = NULL; + *out_max_brightness = -1; + + g_autoptr( GFile ) dir = g_file_new_for_path( "/sys/class/backlight" ); + if ( !dir ) { return; } + + g_autoptr( GFileEnumerator ) enumerator = g_file_enumerate_children( + dir, + "standard::", + G_FILE_QUERY_INFO_NONE, + NULL, + NULL ); + if ( !enumerator ) { return; } + + g_autoptr( GFileInfo ) info = g_file_enumerator_next_file( enumerator, NULL, NULL ); + g_file_enumerator_close( enumerator, NULL, NULL ); + if ( !info ) { return; } + + gchar* device_name = g_strdup( g_file_info_get_name( info ) ); + gchar* current_file_path = g_strdup_printf( "/sys/class/backlight/%s/brightness", device_name ); + gchar* max_file_path = g_strdup_printf( "/sys/class/backlight/%s/max_brightness", device_name ); + *out_device_name = device_name; + *out_file_path = current_file_path; + *out_max_brightness = foobar_brightness_service_read_value( max_file_path ); + g_free( max_file_path ); +} + +// +// Read the current brightness integer (not percentage) from the given file path. +// +gint foobar_brightness_service_read_value( gchar const* path ) +{ + g_autofree gchar* contents = NULL; + g_autoptr( GError ) error = NULL; + if ( !g_file_get_contents( path, &contents, NULL, &error ) ) + { + g_warning( "Unable to read %s: %s", path, error->message ); + return -1; + } + + errno = 0; + char* endptr; + long result = strtol( contents, &endptr, 10 ); + if ( errno != 0 || endptr == contents || result > INT_MAX || result < 0 ) + { + g_warning( "Expected integer in %s, but got: %s", path, contents ); + return -1; + } + + return (gint)result; +} + +// +// Set up a file monitor for the given path. +// +GFileMonitor* foobar_brightness_service_monitor( gchar const* path ) +{ + g_autoptr( GFile ) file = g_file_new_for_path( path ); + if ( !file ) { return NULL; } + + g_autoptr( GError ) error = NULL; + GFileMonitor* monitor = g_file_monitor_file( file, G_FILE_MONITOR_NONE, NULL, &error ); + if ( !monitor ) { g_warning( "Unable to monitor brightness: %s", error->message ); } + + return monitor; +} diff --git a/csrc/services/brightness-service.h b/csrc/services/brightness-service.h new file mode 100644 index 0000000..2b3799e --- /dev/null +++ b/csrc/services/brightness-service.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_BRIGHTNESS_SERVICE foobar_brightness_service_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarBrightnessService, foobar_brightness_service, FOOBAR, BRIGHTNESS_SERVICE, GObject ) + +FoobarBrightnessService* foobar_brightness_service_new ( void ); +gint foobar_brightness_service_get_percentage( FoobarBrightnessService* self ); +void foobar_brightness_service_set_percentage( FoobarBrightnessService* self, + gint percentage ); + +G_END_DECLS diff --git a/csrc/services/clock-service.c b/csrc/services/clock-service.c new file mode 100644 index 0000000..c957011 --- /dev/null +++ b/csrc/services/clock-service.c @@ -0,0 +1,141 @@ +#include "services/clock-service.h" + +// +// FoobarClockService: +// +// Service providing an auto-updating timestamp value which can be used to display the current time. +// + +struct _FoobarClockService +{ + GObject parent_instance; + GDateTime* time; + guint source_id; +}; + +enum +{ + PROP_TIME = 1, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_clock_service_class_init ( FoobarClockServiceClass* klass ); +static void foobar_clock_service_init ( FoobarClockService* self ); +static void foobar_clock_service_get_property( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_clock_service_finalize ( GObject* object ); +static gboolean foobar_clock_service_handle_tick ( gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarClockService, foobar_clock_service, G_TYPE_OBJECT ) + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the clock service. +// +void foobar_clock_service_class_init( FoobarClockServiceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_clock_service_get_property; + object_klass->finalize = foobar_clock_service_finalize; + + props[PROP_TIME] = g_param_spec_boxed( + "time", + "Time", + "The current time, regularly updated.", + G_TYPE_DATE_TIME, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for the clock service. +// +void foobar_clock_service_init( FoobarClockService* self ) +{ + self->time = g_date_time_new_now_local( ); + self->source_id = g_timeout_add_seconds( 1, foobar_clock_service_handle_tick, self ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_clock_service_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarClockService* self = (FoobarClockService*)object; + + switch ( prop_id ) + { + case PROP_TIME: + g_value_set_boxed( value, foobar_clock_service_get_time( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for the clock service. +// +void foobar_clock_service_finalize( GObject* object ) +{ + FoobarClockService* self = (FoobarClockService*)object; + + g_clear_pointer( &self->time, g_date_time_unref ); + g_clear_handle_id( &self->source_id, g_source_remove ); + + G_OBJECT_CLASS( foobar_clock_service_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new clock service instance. +// +FoobarClockService* foobar_clock_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_CLOCK_SERVICE, NULL ); +} + +// +// Get the current snapshot of the timestamp. +// +// This may be a bit older than the value returned by g_date_time_new_now_local. +// +GDateTime* foobar_clock_service_get_time( FoobarClockService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CLOCK_SERVICE( self ), NULL ); + + return self->time; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called every time the update delay has elapsed to update the current timestamp value. +// +gboolean foobar_clock_service_handle_tick( gpointer userdata ) +{ + FoobarClockService* self = (FoobarClockService*)userdata; + + g_clear_pointer( &self->time, g_date_time_unref ); + self->time = g_date_time_new_now_local( ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_TIME] ); + + return G_SOURCE_CONTINUE; +} diff --git a/csrc/services/clock-service.h b/csrc/services/clock-service.h new file mode 100644 index 0000000..d435d92 --- /dev/null +++ b/csrc/services/clock-service.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_CLOCK_SERVICE foobar_clock_service_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarClockService, foobar_clock_service, FOOBAR, CLOCK_SERVICE, GObject ) + +FoobarClockService* foobar_clock_service_new ( void ); +GDateTime* foobar_clock_service_get_time( FoobarClockService* self ); + +G_END_DECLS diff --git a/csrc/services/configuration-service.c b/csrc/services/configuration-service.c new file mode 100644 index 0000000..f3ca010 --- /dev/null +++ b/csrc/services/configuration-service.c @@ -0,0 +1,3248 @@ +#include "services/configuration-service.h" +#include + +#define UPDATE_APPLY_DELAY 250 +#define CONFIGURATION_LOG_DOMAIN "foobar.conf" +#define CONFIGURATION_WARNING( ... ) g_log( CONFIGURATION_LOG_DOMAIN, G_LOG_LEVEL_WARNING, __VA_ARGS__ ) + +// +// FoobarScreenEdge: +// +// An edge of the screen. +// + +G_STATIC_ASSERT( sizeof( FoobarScreenEdge ) == sizeof( gint ) ); +G_DEFINE_ENUM_TYPE( + FoobarScreenEdge, + foobar_screen_edge, + G_DEFINE_ENUM_VALUE( FOOBAR_SCREEN_EDGE_LEFT, "left" ), + G_DEFINE_ENUM_VALUE( FOOBAR_SCREEN_EDGE_RIGHT, "right" ), + G_DEFINE_ENUM_VALUE( FOOBAR_SCREEN_EDGE_TOP, "top" ), + G_DEFINE_ENUM_VALUE( FOOBAR_SCREEN_EDGE_BOTTOM, "bottom" ) ) + +// +// FoobarOrientation: +// +// An orientation value. +// + +G_STATIC_ASSERT( sizeof( FoobarOrientation ) == sizeof( gint ) ); +G_DEFINE_ENUM_TYPE( + FoobarOrientation, + foobar_orientation, + G_DEFINE_ENUM_VALUE( FOOBAR_ORIENTATION_HORIZONTAL, "horizontal" ), + G_DEFINE_ENUM_VALUE( FOOBAR_ORIENTATION_VERTICAL, "vertical" ) ) + +// +// FoobarStatusItem: +// +// An item to display in a status panel item. +// + +G_STATIC_ASSERT( sizeof( FoobarStatusItem ) == sizeof( gint ) ); +G_DEFINE_ENUM_TYPE( + FoobarStatusItem, + foobar_status_item, + G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_NETWORK, "network" ), + G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_BLUETOOTH, "bluetooth" ), + G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_BATTERY, "battery" ), + G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_BRIGHTNESS, "brightness" ), + G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_AUDIO, "audio" ), + G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_NOTIFICATIONS, "notifications" ) ) + +// +// FoobarPanelItemKind: +// +// Enum representing the various panel item types. +// + +G_STATIC_ASSERT( sizeof( FoobarPanelItemKind ) == sizeof( gint ) ); +G_DEFINE_ENUM_TYPE( + FoobarPanelItemKind, + foobar_panel_item_kind, + G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_KIND_ICON, "icon" ), + G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_KIND_CLOCK, "clock" ), + G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_KIND_WORKSPACES, "workspaces" ), + G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_KIND_STATUS, "status" ) ) + +// +// FoobarPanelItemAction: +// +// The action to be executed when a panel item is clicked (if supported by the item). +// + +G_STATIC_ASSERT( sizeof( FoobarPanelItemAction ) == sizeof( gint ) ); +G_DEFINE_ENUM_TYPE( + FoobarPanelItemAction, + foobar_panel_item_action, + G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_ACTION_NONE, "none" ), + G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_ACTION_LAUNCHER, "launcher" ), + G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_ACTION_CONTROL_CENTER, "control-center" ) ) + +// +// FoobarPanelItemPosition: +// +// Position of a panel item in the panel. +// + +G_STATIC_ASSERT( sizeof( FoobarPanelItemPosition ) == sizeof( gint ) ); +G_DEFINE_ENUM_TYPE( + FoobarPanelItemPosition, + foobar_panel_item_position, + G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_POSITION_START, "start" ), + G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_POSITION_CENTER, "center" ), + G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_POSITION_END, "end" ) ) + +// +// FoobarControlCenterRow: +// +// A row in the "controls" section of the control center. +// + +G_STATIC_ASSERT( sizeof( FoobarControlCenterRow ) == sizeof( gint ) ); +G_DEFINE_ENUM_TYPE( + FoobarControlCenterRow, + foobar_control_center_row, + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ROW_CONNECTIVITY, "connectivity" ), + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ROW_AUDIO_OUTPUT, "audio-output" ), + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ROW_AUDIO_INPUT, "audio-input" ), + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ROW_BRIGHTNESS, "brightness" ) ) + +// +// FoobarControlCenterAlignment: +// +// Alignment of the control center along its screen edge. +// + +G_STATIC_ASSERT( sizeof( FoobarControlCenterAlignment ) == sizeof( gint ) ); +G_DEFINE_ENUM_TYPE( + FoobarControlCenterAlignment, + foobar_control_center_alignment, + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ALIGNMENT_START, "start" ), + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ALIGNMENT_CENTER, "center" ), + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ALIGNMENT_END, "end" ), + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL, "fill" ) ) + +// +// FoobarGeneralConfiguration: +// +// General application settings not specific to a single component. +// + +struct _FoobarGeneralConfiguration +{ + gchar* stylesheet; +}; + +static void foobar_general_configuration_load ( FoobarGeneralConfiguration* self, + GKeyFile* file ); +static void foobar_general_configuration_store( FoobarGeneralConfiguration const* self, + GKeyFile* file ); + +G_DEFINE_BOXED_TYPE( + FoobarGeneralConfiguration, + foobar_general_configuration, + foobar_general_configuration_copy, + foobar_general_configuration_free ) + +// +// FoobarPanelItemConfiguration: +// +// Configuration for an item in the panel. Structures of this type have a specific item kind with more properties. +// + +struct _FoobarPanelItemConfiguration +{ + FoobarPanelItemKind kind; + gchar* name; + FoobarPanelItemPosition position; + FoobarPanelItemAction action; + union + { + struct + { + gchar* icon_name; + } icon; + + struct + { + gchar* format; + } clock; + + struct + { + gint button_size; + gint spacing; + } workspaces; + + struct + { + FoobarStatusItem* items; + gsize items_count; + gint spacing; + gboolean show_labels; + gboolean enable_scrolling; + } status; + }; +}; + +static FoobarPanelItemConfiguration* foobar_panel_item_configuration_load ( GKeyFile* file, + gchar const* group ); +static void foobar_panel_item_configuration_store( FoobarPanelItemConfiguration const* self, + GKeyFile* file ); + +G_DEFINE_BOXED_TYPE( + FoobarPanelItemConfiguration, + foobar_panel_item_configuration, + foobar_panel_item_configuration_copy, + foobar_panel_item_configuration_free ) + +// +// FoobarPanelConfiguration: +// +// Configuration for the panel shown at the edge of a screen. +// + +struct _FoobarPanelConfiguration +{ + FoobarScreenEdge position; + gint margin; + gint padding; + gint size; + gint spacing; + gboolean multi_monitor; + FoobarPanelItemConfiguration** items; + gsize items_count; +}; + +static void foobar_panel_configuration_load ( FoobarPanelConfiguration* self, + GKeyFile* file ); +static void foobar_panel_configuration_store( FoobarPanelConfiguration const* self, + GKeyFile* file ); + +G_DEFINE_BOXED_TYPE( + FoobarPanelConfiguration, + foobar_panel_configuration, + foobar_panel_configuration_copy, + foobar_panel_configuration_free ) + +// +// FoobarLauncherConfiguration: +// +// Configuration for the application launcher. +// + +struct _FoobarLauncherConfiguration +{ + gint width; + gint position; + gint max_height; +}; + +static void foobar_launcher_configuration_load ( FoobarLauncherConfiguration* self, + GKeyFile* file ); +static void foobar_launcher_configuration_store( FoobarLauncherConfiguration const* self, + GKeyFile* file ); + +G_DEFINE_BOXED_TYPE( + FoobarLauncherConfiguration, + foobar_launcher_configuration, + foobar_launcher_configuration_copy, + foobar_launcher_configuration_free ) + +// +// FoobarControlCenterConfiguration: +// +// Configuration for the control center. +// + +struct _FoobarControlCenterConfiguration +{ + gint width; + gint height; + FoobarScreenEdge position; + gint offset; + gint padding; + gint spacing; + FoobarOrientation orientation; + FoobarControlCenterAlignment alignment; + FoobarControlCenterRow* rows; + gsize rows_count; +}; + +static void foobar_control_center_configuration_load ( FoobarControlCenterConfiguration* self, + GKeyFile* file ); +static void foobar_control_center_configuration_store( FoobarControlCenterConfiguration const* self, + GKeyFile* file ); + +G_DEFINE_BOXED_TYPE( + FoobarControlCenterConfiguration, + foobar_control_center_configuration, + foobar_control_center_configuration_copy, + foobar_control_center_configuration_free ) + +// +// FoobarNotificationConfiguration: +// +// Configuration for notifications and the notification area shown in the corner of the screen. +// + +struct _FoobarNotificationConfiguration +{ + gint width; + gint min_height; + gint spacing; + gint close_button_inset; + gchar* time_format; +}; + +static void foobar_notification_configuration_load ( FoobarNotificationConfiguration* self, + GKeyFile* file ); +static void foobar_notification_configuration_store( FoobarNotificationConfiguration const* self, + GKeyFile* file ); + +G_DEFINE_BOXED_TYPE( + FoobarNotificationConfiguration, + foobar_notification_configuration, + foobar_notification_configuration_copy, + foobar_notification_configuration_free ) + +// +// FoobarConfiguration: +// +// Main configuration structure containing the configuration state for all components of the application. +// + +struct _FoobarConfiguration +{ + FoobarGeneralConfiguration* general; + FoobarPanelConfiguration* panel; + FoobarLauncherConfiguration* launcher; + FoobarControlCenterConfiguration* control_center; + FoobarNotificationConfiguration* notifications; +}; + +static void foobar_configuration_load ( FoobarConfiguration* self, + GKeyFile* file ); +static void foobar_configuration_load_from_file( FoobarConfiguration* self, + gchar const* path ); +static void foobar_configuration_store ( FoobarConfiguration const* self, + GKeyFile* file ); + +G_DEFINE_BOXED_TYPE( FoobarConfiguration, foobar_configuration, foobar_configuration_copy, foobar_configuration_free ) + +// +// FoobarConfigurationService: +// +// Service monitoring the configuration state of the application specified in its config file. +// + +struct _FoobarConfigurationService +{ + GObject parent_instance; + gchar* path; + char* actual_path; + FoobarConfiguration* current; + GFileMonitor* monitor; + GFileMonitor* actual_monitor; + gulong changed_handler_id; + gulong actual_changed_handler_id; + guint update_source_id; +}; + +enum +{ + PROP_CURRENT = 1, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_configuration_service_class_init ( FoobarConfigurationServiceClass* klass ); +static void foobar_configuration_service_init ( FoobarConfigurationService* self ); +static void foobar_configuration_service_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_configuration_service_finalize ( GObject* object ); +static void foobar_configuration_service_handle_changed ( GFileMonitor* monitor, + GFile* file, + GFile* other_file, + GFileMonitorEvent event_type, + gpointer userdata ); +static void foobar_configuration_service_handle_actual_changed( GFileMonitor* monitor, + GFile* file, + GFile* other_file, + GFileMonitorEvent event_type, + gpointer userdata ); +static void foobar_configuration_service_update ( FoobarConfigurationService* self ); +static gboolean foobar_configuration_service_update_cb ( gpointer userdata ); +static GFileMonitor* foobar_configuration_service_create_monitor ( gchar const* path ); + +G_DEFINE_FINAL_TYPE( FoobarConfigurationService, foobar_configuration_service, G_TYPE_OBJECT ) + +// +// Parsing/Serialization Helpers: +// +// These functions make it easier to parse, validate and serialize various data types. +// + +typedef enum +{ + VALIDATE_NONE = 0, + VALIDATE_NON_NEGATIVE = 1 << 0, + VALIDATE_NON_ZERO = 1 << 1, + VALIDATE_POSITIVE = VALIDATE_NON_NEGATIVE | VALIDATE_NON_ZERO, + VALIDATE_FILE_URL = 1 << 2, + VALIDATE_DISTINCT = 1 << 3, +} ValidationFlags; + +static gboolean try_get_string_value ( GKeyFile* file, + gchar const* group, + gchar const* key, + ValidationFlags validation, + gchar** out ); +static gboolean try_get_string_list_value( GKeyFile* file, + gchar const* group, + gchar const* key, + ValidationFlags validation, + GStrv* out, + gsize* out_count ); +static gboolean try_get_int_value ( GKeyFile* file, + gchar const* group, + gchar const* key, + ValidationFlags validation, + gint* out ); +static gboolean try_get_enum_value ( GKeyFile* file, + gchar const* group, + gchar const* key, + GType enum_type, + ValidationFlags validation, + gint* out ); +static gboolean try_get_enum_list_value ( GKeyFile* file, + gchar const* group, + gchar const* key, + GType enum_type, + ValidationFlags validation, + gpointer* out, + gsize* out_count ); +static gboolean try_get_boolean_value ( GKeyFile* file, + gchar const* group, + gchar const* key, + ValidationFlags validation, + gboolean* out ); + +static gchar const* enum_value_to_string ( GType enum_type, + gint value ); +static gchar const** enum_list_to_string ( GType enum_type, + gconstpointer values, + gsize values_count ); +static gchar* enum_format_help_text( GType enum_type ); + +// --------------------------------------------------------------------------------------------------------------------- +// Default Configuration +// --------------------------------------------------------------------------------------------------------------------- + +static FoobarGeneralConfiguration default_general_configuration = + { + .stylesheet = "resource:///foobar/styles/default.css", + }; + +static FoobarPanelItemConfiguration default_panel_item_icon_configuration = + { + .kind = FOOBAR_PANEL_ITEM_KIND_ICON, + .name = "launcher", + .position = FOOBAR_PANEL_ITEM_POSITION_START, + .action = FOOBAR_PANEL_ITEM_ACTION_LAUNCHER, + .icon.icon_name = "fluent-grid-dots-symbolic", + }; + +static FoobarPanelItemConfiguration default_panel_item_clock_configuration = + { + .kind = FOOBAR_PANEL_ITEM_KIND_CLOCK, + .name = "clock", + .position = FOOBAR_PANEL_ITEM_POSITION_CENTER, + .action = FOOBAR_PANEL_ITEM_ACTION_NONE, + .clock.format = "%H\n%M", + }; + +static FoobarPanelItemConfiguration default_panel_item_workspaces_configuration = + { + .kind = FOOBAR_PANEL_ITEM_KIND_WORKSPACES, + .name = "workspaces", + .position = FOOBAR_PANEL_ITEM_POSITION_START, + .action = FOOBAR_PANEL_ITEM_ACTION_NONE, + .workspaces.button_size = 20, + .workspaces.spacing = 6, + }; + +static FoobarStatusItem default_panel_item_status_configuration_items[] = + { + FOOBAR_STATUS_ITEM_BATTERY, + FOOBAR_STATUS_ITEM_BRIGHTNESS, + FOOBAR_STATUS_ITEM_AUDIO, + FOOBAR_STATUS_ITEM_NETWORK, + FOOBAR_STATUS_ITEM_BLUETOOTH, + FOOBAR_STATUS_ITEM_NOTIFICATIONS + }; + +static FoobarPanelItemConfiguration default_panel_item_status_configuration = + { + .kind = FOOBAR_PANEL_ITEM_KIND_STATUS, + .name = "status", + .position = FOOBAR_PANEL_ITEM_POSITION_END, + .action = FOOBAR_PANEL_ITEM_ACTION_CONTROL_CENTER, + .status.items = default_panel_item_status_configuration_items, + .status.items_count = G_N_ELEMENTS( default_panel_item_status_configuration_items ), + .status.spacing = 6, + .status.show_labels = FALSE, + .status.enable_scrolling = FALSE, + }; + +static FoobarPanelItemConfiguration *default_panel_configuration_items[] = + { + &default_panel_item_icon_configuration, + &default_panel_item_workspaces_configuration, + &default_panel_item_clock_configuration, + &default_panel_item_status_configuration, + NULL, + }; + +static FoobarPanelConfiguration default_panel_configuration = + { + .position = FOOBAR_SCREEN_EDGE_LEFT, + .margin = 16, + .padding = 12, + .size = 48, + .spacing = 12, + .multi_monitor = TRUE, + .items = default_panel_configuration_items, + .items_count = G_N_ELEMENTS( default_panel_configuration_items ) - 1, + }; + +static FoobarLauncherConfiguration default_launcher_configuration = + { + .width = 600, + .position = 300, + .max_height = 400, + }; + +static FoobarControlCenterRow default_control_center_configuration_rows[] = + { + FOOBAR_CONTROL_CENTER_ROW_CONNECTIVITY, + FOOBAR_CONTROL_CENTER_ROW_AUDIO_OUTPUT, + FOOBAR_CONTROL_CENTER_ROW_BRIGHTNESS, + }; + +static FoobarControlCenterConfiguration default_control_center_configuration = + { + .position = FOOBAR_SCREEN_EDGE_LEFT, + .width = 400, + .height = 600, + .offset = 16, + .padding = 24, + .spacing = 12, + .orientation = FOOBAR_ORIENTATION_VERTICAL, + .alignment = FOOBAR_CONTROL_CENTER_ALIGNMENT_CENTER, + .rows = default_control_center_configuration_rows, + .rows_count = G_N_ELEMENTS( default_control_center_configuration_rows ), + }; + +static FoobarNotificationConfiguration default_notification_configuration = + { + .width = 400, + .min_height = 48, + .spacing = 16, + .close_button_inset = -6, + .time_format = "%H:%M", + }; + +static FoobarConfiguration default_configuration = + { + .general = &default_general_configuration, + .panel = &default_panel_configuration, + .launcher = &default_launcher_configuration, + .control_center = &default_control_center_configuration, + .notifications = &default_notification_configuration, + }; + +// --------------------------------------------------------------------------------------------------------------------- +// Default Configuration +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a default general configuration structure. +// +FoobarGeneralConfiguration* foobar_general_configuration_new( void ) +{ + return foobar_general_configuration_copy( &default_general_configuration ); +} + +// +// Create a mutable copy of another general configuration structure. +// +FoobarGeneralConfiguration* foobar_general_configuration_copy( FoobarGeneralConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + + FoobarGeneralConfiguration* copy = g_new0( FoobarGeneralConfiguration, 1 ); + copy->stylesheet = g_strdup( self->stylesheet ); + return copy; +} + +// +// Release resources associated with a general configuration structure. +// +void foobar_general_configuration_free( FoobarGeneralConfiguration* self ) +{ + g_return_if_fail( self != NULL ); + + g_free( self->stylesheet ); + g_free( self ); +} + +// +// Check if two general configuration structures are equal. +// +gboolean foobar_general_configuration_equal( + FoobarGeneralConfiguration const* a, + FoobarGeneralConfiguration const* b ) +{ + g_return_val_if_fail( a != NULL, FALSE ); + g_return_val_if_fail( b != NULL, FALSE ); + + if ( g_strcmp0( a->stylesheet, b->stylesheet ) ) { return FALSE; } + + return TRUE; +} + +// +// URI to the CSS stylesheet to use -- this may be a resource that's bundled with Foobar or a "file:" URI. +// +gchar const* foobar_general_configuration_get_stylesheet( FoobarGeneralConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + return self->stylesheet; +} + +// +// URI to the CSS stylesheet to use -- this may be a resource that's bundled with Foobar or a "file:" URI. +// +void foobar_general_configuration_set_stylesheet( + FoobarGeneralConfiguration* self, + gchar const* value ) +{ + g_return_if_fail( self != NULL ); + g_return_if_fail( value != NULL ); + + g_free( self->stylesheet ); + self->stylesheet = g_strdup( value ); +} + +// +// Populate a general configuration structure from the "general" section of a keyfile. +// +void foobar_general_configuration_load( + FoobarGeneralConfiguration* self, + GKeyFile* file ) +{ + g_autofree gchar* stylesheet = NULL; + if ( try_get_string_value( file, "general", "stylesheet", VALIDATE_FILE_URL, &stylesheet ) ) + { + foobar_general_configuration_set_stylesheet( self, stylesheet ); + } +} + +// +// Store a general configuration structure in the "general" section of a keyfile. +// +void foobar_general_configuration_store( + FoobarGeneralConfiguration const* self, + GKeyFile* file ) +{ + gchar const* stylesheet = foobar_general_configuration_get_stylesheet( self ); + g_key_file_set_string( file, "general", "stylesheet", stylesheet ); + g_key_file_set_comment( + file, + "general", + "stylesheet", + " URI to the CSS stylesheet to use -- this may be a resource that's bundled with Foobar or a \"file:\" URI.", + NULL ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Panel Item Configuration +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a default icon panel item configuration structure. +// +FoobarPanelItemConfiguration* foobar_panel_item_icon_configuration_new( void ) +{ + return foobar_panel_item_configuration_copy( &default_panel_item_icon_configuration ); +} + +// +// Create a default clock panel item configuration structure. +// +FoobarPanelItemConfiguration* foobar_panel_item_clock_configuration_new( void ) +{ + return foobar_panel_item_configuration_copy( &default_panel_item_clock_configuration ); +} + +// +// Create a default workspaces panel item configuration structure. +// +FoobarPanelItemConfiguration* foobar_panel_item_workspaces_configuration_new( void ) +{ + return foobar_panel_item_configuration_copy( &default_panel_item_workspaces_configuration ); +} + +// +// Create a default status panel item configuration structure. +// +FoobarPanelItemConfiguration* foobar_panel_item_status_configuration_new( void ) +{ + return foobar_panel_item_configuration_copy( &default_panel_item_status_configuration ); +} + +// +// Create a mutable copy of another panel item configuration structure, preserving its item type. +// +FoobarPanelItemConfiguration* foobar_panel_item_configuration_copy( FoobarPanelItemConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + + FoobarPanelItemConfiguration* copy = g_new0( FoobarPanelItemConfiguration, 1 ); + copy->kind = self->kind; + copy->name = g_strdup( self->name ); + copy->position = self->position; + copy->action = self->action; + switch ( copy->kind ) + { + case FOOBAR_PANEL_ITEM_KIND_ICON: + copy->icon.icon_name = g_strdup( self->icon.icon_name ); + break; + case FOOBAR_PANEL_ITEM_KIND_CLOCK: + copy->clock.format = g_strdup( self->clock.format ); + break; + case FOOBAR_PANEL_ITEM_KIND_WORKSPACES: + copy->workspaces.button_size = self->workspaces.button_size; + copy->workspaces.spacing = self->workspaces.spacing; + break; + case FOOBAR_PANEL_ITEM_KIND_STATUS: + copy->status.items = g_new( FoobarStatusItem, self->status.items_count ); + copy->status.items_count = self->status.items_count; + memcpy( copy->status.items, self->status.items, sizeof( *copy->status.items ) * copy->status.items_count ); + copy->status.spacing = self->status.spacing; + copy->status.show_labels = self->status.show_labels; + copy->status.enable_scrolling = self->status.enable_scrolling; + break; + default: + g_warn_if_reached( ); + } + return copy; +} + +// +// Release resources associated with a panel item configuration structure. +// +void foobar_panel_item_configuration_free( FoobarPanelItemConfiguration* self ) +{ + g_return_if_fail( self != NULL ); + + switch ( self->kind ) + { + case FOOBAR_PANEL_ITEM_KIND_ICON: + g_free( self->icon.icon_name ); + break; + case FOOBAR_PANEL_ITEM_KIND_CLOCK: + g_free( self->clock.format ); + break; + case FOOBAR_PANEL_ITEM_KIND_WORKSPACES: + break; + case FOOBAR_PANEL_ITEM_KIND_STATUS: + g_free( self->status.items ); + break; + default: + g_warn_if_reached( ); + break; + } + + g_free( self->name ); + g_free( self ); +} + +// +// Check if two panel item configuration structures are equal. +// +gboolean foobar_panel_item_configuration_equal( + FoobarPanelItemConfiguration const* a, + FoobarPanelItemConfiguration const* b ) +{ + g_return_val_if_fail( a != NULL, FALSE ); + g_return_val_if_fail( b != NULL, FALSE ); + + if ( a->kind != b->kind ) { return FALSE; } + if ( a->position != b->position ) { return FALSE; } + if ( a->action != b->action ) { return FALSE; } + if ( g_strcmp0( a->name, b->name ) ) { return FALSE; } + + switch ( a->kind ) + { + case FOOBAR_PANEL_ITEM_KIND_ICON: + if ( g_strcmp0( a->icon.icon_name, b->icon.icon_name ) ) { return FALSE; } + break; + case FOOBAR_PANEL_ITEM_KIND_CLOCK: + if ( g_strcmp0( a->clock.format, b->clock.format ) ) { return FALSE; } + break; + case FOOBAR_PANEL_ITEM_KIND_WORKSPACES: + if ( a->workspaces.button_size != b->workspaces.button_size ) { return FALSE; } + if ( a->workspaces.spacing != b->workspaces.spacing ) { return FALSE; } + break; + case FOOBAR_PANEL_ITEM_KIND_STATUS: + if ( a->status.items_count != b->status.items_count ) { return FALSE; } + if ( memcmp( a->status.items, b->status.items, sizeof( *a->status.items ) * a->status.items_count ) ) { return FALSE; } + if ( a->status.spacing != b->status.spacing ) { return FALSE; } + if ( a->status.show_labels != b->status.show_labels ) { return FALSE; } + if ( a->status.enable_scrolling != b->status.enable_scrolling ) { return FALSE; } + break; + default: + g_warn_if_reached( ); + break; + } + + return TRUE; +} + +// +// The type of panel item to be configured by this structure. +// +FoobarPanelItemKind foobar_panel_item_configuration_get_kind( FoobarPanelItemConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->kind; +} + +// +// Name of the panel item (this is derived from the section name, which has the form "panel.[name]"). +// +gchar const* foobar_panel_item_configuration_get_name( FoobarPanelItemConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->name; +} + +// +// Position where the item should be placed item within the panel. +// +FoobarPanelItemPosition foobar_panel_item_configuration_get_position( FoobarPanelItemConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->position; +} + +// +// Action invoked when the user clicks the item (if supported by the item). +// +FoobarPanelItemAction foobar_panel_item_configuration_get_action( FoobarPanelItemConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->action; +} + +// +// The GTK icon to use for the item. +// +gchar const* foobar_panel_item_icon_configuration_get_icon_name( FoobarPanelItemConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_ICON, NULL ); + + return self->icon.icon_name; +} + +// +// The time format string as used by g_date_time_format. +// +gchar const* foobar_panel_item_clock_configuration_get_format( FoobarPanelItemConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_CLOCK, NULL ); + + return self->clock.format; +} + +// +// Size of each workspace button. +// +gint foobar_panel_item_workspaces_configuration_get_button_size( FoobarPanelItemConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_WORKSPACES, 0 ); + + return self->workspaces.button_size; +} + +// +// Inner spacing between the workspace buttons. +// +gint foobar_panel_item_workspaces_configuration_get_spacing( FoobarPanelItemConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_WORKSPACES, 0 ); + + return self->workspaces.spacing; +} + +// +// Ordered list of status items to display in the item. +// +FoobarStatusItem const* foobar_panel_item_status_configuration_get_items( + FoobarPanelItemConfiguration const* self, + gsize* out_count ) +{ + g_return_val_if_fail( self != NULL, NULL ); + g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS, NULL ); + g_return_val_if_fail( out_count != NULL, NULL ); + + *out_count = self->status.items_count; + return self->status.items; +} + +// +// Inner spacing between the status items. +// +gint foobar_panel_item_status_configuration_get_spacing( FoobarPanelItemConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS, 0 ); + + return self->status.spacing; +} + +// +// Indicates whether text labels should be shown next to the status icons. +// +gboolean foobar_panel_item_status_configuration_get_show_labels( FoobarPanelItemConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, FALSE ); + g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS, FALSE ); + + return self->status.show_labels; +} + +// +// If set to true, some settings like volume or brightness can be adjusted by scrolling while hovering over the status +// item. +// +gboolean foobar_panel_item_status_configuration_get_enable_scrolling( FoobarPanelItemConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, FALSE ); + g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS, FALSE ); + + return self->status.enable_scrolling; +} + +// +// Name of the panel item (this is derived from the section name, which has the form "panel.[name]"). +// +void foobar_panel_item_configuration_set_name( + FoobarPanelItemConfiguration* self, + gchar const* value ) +{ + g_return_if_fail( self != NULL ); + g_return_if_fail( value != NULL ); + + g_free( self->name ); + self->name = g_strdup( value ); +} + +// +// Position where the item should be placed item within the panel. +// +void foobar_panel_item_configuration_set_position( + FoobarPanelItemConfiguration* self, + FoobarPanelItemPosition value ) +{ + g_return_if_fail( self != NULL ); + self->position = value; +} + +// +// Action invoked when the user clicks the item (if supported by the item). +// +void foobar_panel_item_configuration_set_action( + FoobarPanelItemConfiguration* self, + FoobarPanelItemAction value ) +{ + g_return_if_fail( self != NULL ); + self->action = value; +} + +// +// The GTK icon to use for the item. +// +void foobar_panel_item_icon_configuration_set_icon_name( + FoobarPanelItemConfiguration* self, + gchar const* value ) +{ + g_return_if_fail( self != NULL ); + g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_ICON ); + g_return_if_fail( value != NULL ); + + g_free( self->icon.icon_name ); + self->icon.icon_name = g_strdup( value ); +} + +// +// The time format string as used by g_date_time_format. +// +void foobar_panel_item_clock_configuration_set_format( + FoobarPanelItemConfiguration* self, + gchar const* value ) +{ + g_return_if_fail( self != NULL ); + g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_CLOCK ); + g_return_if_fail( value != NULL ); + + g_free( self->clock.format ); + self->clock.format = g_strdup( value ); +} + +// +// Size of each workspace button. +// +void foobar_panel_item_workspaces_configuration_set_button_size( + FoobarPanelItemConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_WORKSPACES ); + + self->workspaces.button_size = value; +} + +// +// Inner spacing between the workspace buttons. +// +void foobar_panel_item_workspaces_configuration_set_spacing( + FoobarPanelItemConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_WORKSPACES ); + + self->workspaces.spacing = value; +} + +// +// Ordered list of status items to display in the item. +// +void foobar_panel_item_status_configuration_set_items( + FoobarPanelItemConfiguration* self, + FoobarStatusItem const* value, + gsize value_count ) +{ + g_return_if_fail( self != NULL ); + g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS ); + g_return_if_fail( value != NULL || value_count == 0 ); + + g_free( self->status.items ); + self->status.items = g_new( FoobarStatusItem, value_count ); + self->status.items_count = value_count; + memcpy( self->status.items, value, sizeof( *self->status.items ) * self->status.items_count ); +} + +// +// Inner spacing between the status items. +// +void foobar_panel_item_status_configuration_set_spacing( + FoobarPanelItemConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS ); + + self->status.spacing = value; +} + +// +// Indicates whether text labels should be shown next to the status icons. +// +void foobar_panel_item_status_configuration_set_show_labels( + FoobarPanelItemConfiguration* self, + gboolean value ) +{ + g_return_if_fail( self != NULL ); + g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS ); + + self->status.show_labels = value; +} + +// +// If set to true, some settings like volume or brightness can be adjusted by scrolling while hovering over the status +// item. +// +void foobar_panel_item_status_configuration_set_enable_scrolling( + FoobarPanelItemConfiguration* self, + gboolean value ) +{ + g_return_if_fail( self != NULL ); + g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS ); + + self->status.enable_scrolling = value; +} + +// +// Create a panel item configuration structure from the given section of a keyfile, returning NULL if it is not a panel +// item configuration section. +// +FoobarPanelItemConfiguration* foobar_panel_item_configuration_load( + GKeyFile* file, + gchar const* group ) +{ + if ( !g_str_has_prefix( group, "panel." ) ) + { + return NULL; + } + + gint kind; + if ( !try_get_enum_value( file, group, "kind", FOOBAR_TYPE_PANEL_ITEM_KIND, VALIDATE_NONE, &kind ) ) + { + return NULL; + } + + FoobarPanelItemConfiguration* result; + switch ( (FoobarPanelItemKind)kind ) + { + case FOOBAR_PANEL_ITEM_KIND_ICON: + { + result = foobar_panel_item_icon_configuration_new( ); + + g_autofree gchar* icon_name = NULL; + if ( try_get_string_value( file, group, "icon-name", VALIDATE_NONE, &icon_name ) ) + { + foobar_panel_item_icon_configuration_set_icon_name( result, icon_name ); + } + + break; + } + case FOOBAR_PANEL_ITEM_KIND_CLOCK: + { + result = foobar_panel_item_clock_configuration_new( ); + + g_autofree gchar* format = NULL; + if ( try_get_string_value( file, group, "format", VALIDATE_NONE, &format ) ) + { + foobar_panel_item_clock_configuration_set_format( result, format ); + } + + break; + } + case FOOBAR_PANEL_ITEM_KIND_WORKSPACES: + { + result = foobar_panel_item_workspaces_configuration_new( ); + + gint button_size; + if ( try_get_int_value( file, group, "button-size", VALIDATE_POSITIVE, &button_size ) ) + { + foobar_panel_item_workspaces_configuration_set_button_size( result, button_size ); + } + + gint spacing; + if ( try_get_int_value( file, group, "spacing", VALIDATE_NON_NEGATIVE, &spacing ) ) + { + foobar_panel_item_workspaces_configuration_set_spacing( result, spacing ); + } + + break; + } + case FOOBAR_PANEL_ITEM_KIND_STATUS: + { + result = foobar_panel_item_status_configuration_new( ); + + g_autofree gpointer items = NULL; + gsize items_count; + if ( try_get_enum_list_value( file, group, "items", FOOBAR_TYPE_STATUS_ITEM, VALIDATE_DISTINCT, &items, &items_count ) ) + { + foobar_panel_item_status_configuration_set_items( result, items, items_count ); + } + + gint spacing; + if ( try_get_int_value( file, group, "spacing", VALIDATE_NON_NEGATIVE, &spacing ) ) + { + foobar_panel_item_status_configuration_set_spacing( result, spacing ); + } + + gboolean show_labels; + if ( try_get_boolean_value( file, group, "show-labels", VALIDATE_NONE, &show_labels ) ) + { + foobar_panel_item_status_configuration_set_show_labels( result, show_labels ); + } + + gboolean enable_scrolling; + if ( try_get_boolean_value( file, group, "enable-scrolling", VALIDATE_NONE, &enable_scrolling ) ) + { + foobar_panel_item_status_configuration_set_enable_scrolling( result, enable_scrolling ); + } + + break; + } + default: + { + g_warn_if_reached( ); + return NULL; + } + } + + gchar const* name = group + strlen( "panel." ); + foobar_panel_item_configuration_set_name( result, name ); + + gint position; + if ( try_get_enum_value( file, group, "position", FOOBAR_TYPE_PANEL_ITEM_POSITION, VALIDATE_NONE, &position ) ) + { + foobar_panel_item_configuration_set_position( result, position ); + } + + gint action; + if ( try_get_enum_value( file, group, "action", FOOBAR_TYPE_PANEL_ITEM_ACTION, VALIDATE_NONE, &action ) ) + { + foobar_panel_item_configuration_set_action( result, action ); + } + + return result; +} + +// +// Store a panel item configuration structure in the "panel.[name]" section of a keyfile. +// +void foobar_panel_item_configuration_store( + FoobarPanelItemConfiguration const* self, + GKeyFile* file ) +{ + gchar const* name = foobar_panel_item_configuration_get_name( self ); + g_autofree gchar* group = g_strdup_printf( "panel.%s", name ); + + FoobarPanelItemKind kind = foobar_panel_item_configuration_get_kind( self ); + g_key_file_set_string( file, group, "kind", enum_value_to_string( FOOBAR_TYPE_PANEL_ITEM_KIND, kind ) ); + + FoobarPanelItemPosition position = foobar_panel_item_configuration_get_position( self ); + g_key_file_set_string( file, group, "position", enum_value_to_string( FOOBAR_TYPE_PANEL_ITEM_POSITION, position ) ); + + FoobarPanelItemAction action = foobar_panel_item_configuration_get_action( self ); + g_key_file_set_string( file, group, "action", enum_value_to_string( FOOBAR_TYPE_PANEL_ITEM_ACTION, action ) ); + + switch ( kind ) + { + case FOOBAR_PANEL_ITEM_KIND_ICON: + { + gchar const* icon_name = foobar_panel_item_icon_configuration_get_icon_name( self ); + g_key_file_set_string( file, group, "icon-name", icon_name ); + g_key_file_set_comment( + file, + group, + "icon-name", + " The GTK icon to use for the item.", + NULL ); + + break; + } + case FOOBAR_PANEL_ITEM_KIND_CLOCK: + { + gchar const* format = foobar_panel_item_clock_configuration_get_format( self ); + g_key_file_set_string( file, group, "format", format ); + g_key_file_set_comment( + file, + group, + "format", + " The time format string as used by g_date_time_format.", + NULL ); + + break; + } + case FOOBAR_PANEL_ITEM_KIND_WORKSPACES: + { + gint button_size = foobar_panel_item_workspaces_configuration_get_button_size( self ); + g_key_file_set_integer( file, group, "button-size", button_size ); + g_key_file_set_comment( + file, + group, + "button-size", + " Size of each workspace button.", + NULL ); + + gint spacing = foobar_panel_item_workspaces_configuration_get_spacing( self ); + g_key_file_set_integer( file, group, "spacing", spacing ); + g_key_file_set_comment( + file, + group, + "spacing", + " Inner spacing between the workspace buttons.", + NULL ); + + break; + } + case FOOBAR_PANEL_ITEM_KIND_STATUS: + { + gsize items_count; + FoobarStatusItem const* items = foobar_panel_item_status_configuration_get_items( self, &items_count ); + g_autofree gchar const** items_str = enum_list_to_string( FOOBAR_TYPE_STATUS_ITEM, items, items_count ); + g_key_file_set_string_list( file, group, "items", items_str, items_count ); + g_key_file_set_comment( + file, + group, + "items", + " Ordered list of status items to display in the item.", + NULL ); + + gint spacing = foobar_panel_item_status_configuration_get_spacing( self ); + g_key_file_set_integer( file, group, "spacing", spacing ); + g_key_file_set_comment( + file, + group, + "spacing", + " Inner spacing between the status items.", + NULL ); + + gboolean show_labels = foobar_panel_item_status_configuration_get_show_labels( self ); + g_key_file_set_boolean( file, group, "show-labels", show_labels ); + g_key_file_set_comment( + file, + group, + "show-labels", + " Indicates whether text labels should be shown next to the status icons.", + NULL ); + + gboolean enable_scrolling = foobar_panel_item_status_configuration_get_enable_scrolling( self ); + g_key_file_set_boolean( file, group, "enable-scrolling", enable_scrolling ); + g_key_file_set_comment( + file, + group, + "enable-scrolling", + " If set to true, some settings like volume or brightness can be adjusted by scrolling while hovering" + " over the status item.", + NULL ); + + break; + } + default: + { + g_warn_if_reached( ); + break; + } + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Panel Configuration +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a default panel configuration structure. +// +FoobarPanelConfiguration* foobar_panel_configuration_new( void ) +{ + return foobar_panel_configuration_copy( &default_panel_configuration ); +} + +// +// Create a mutable copy of another panel configuration structure. +// +FoobarPanelConfiguration* foobar_panel_configuration_copy( FoobarPanelConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + + FoobarPanelConfiguration* copy = g_new0( FoobarPanelConfiguration, 1 ); + copy->position = self->position; + copy->margin = self->margin; + copy->padding = self->padding; + copy->size = self->size; + copy->spacing = self->spacing; + copy->multi_monitor = self->multi_monitor; + copy->items = g_new( FoobarPanelItemConfiguration*, self->items_count + 1 ); + copy->items_count = self->items_count; + for ( gsize i = 0; i < copy->items_count; ++i ) + { + copy->items[i] = foobar_panel_item_configuration_copy( self->items[i] ); + } + return copy; +} + +// +// Release resources associated with a panel configuration structure. +// +void foobar_panel_configuration_free( FoobarPanelConfiguration* self ) +{ + g_return_if_fail( self != NULL ); + + for ( gsize i = 0; i < self->items_count; ++i ) + { + foobar_panel_item_configuration_free( self->items[i] ); + } + g_free( self->items ); + g_free( self ); +} + +// +// Check if two panel configuration structures are equal. +// +gboolean foobar_panel_configuration_equal( + FoobarPanelConfiguration const* a, + FoobarPanelConfiguration const* b ) +{ + g_return_val_if_fail( a != NULL, FALSE ); + g_return_val_if_fail( b != NULL, FALSE ); + + if ( a->position != b->position ) { return FALSE; } + if ( a->margin != b->margin ) { return FALSE; } + if ( a->padding != b->padding ) { return FALSE; } + if ( a->size != b->size ) { return FALSE; } + if ( a->spacing != b->spacing ) { return FALSE; } + if ( a->multi_monitor != b->multi_monitor ) { return FALSE; } + if ( a->items_count != b->items_count ) { return FALSE; } + + for ( gsize i = 0; i < a->items_count; ++i ) + { + if ( !foobar_panel_item_configuration_equal( a->items[i], b->items[i] ) ) { return FALSE; } + } + + return TRUE; +} + +// +// The screen edge which the panel should appear on. +// +FoobarScreenEdge foobar_panel_configuration_get_position( FoobarPanelConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->position; +} + +// +// Offset of the panel from all screen edges. +// +gint foobar_panel_configuration_get_margin( FoobarPanelConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->margin; +} + +// +// Inset of the panel along its orientation. +// +gint foobar_panel_configuration_get_padding( FoobarPanelConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->padding; +} + +// +// Size of the panel (depending on the orientation, this can be either the width or the height). +// +gint foobar_panel_configuration_get_size( FoobarPanelConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->size; +} + +// +// Spacing between panel items. +// +gint foobar_panel_configuration_get_spacing( FoobarPanelConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->spacing; +} + +// +// Flag to enable the panel on all monitors. +// +gboolean foobar_panel_configuration_get_multi_monitor( FoobarPanelConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, FALSE ); + return self->multi_monitor; +} + +// +// Items configured to be displayed in the panel. +// +FoobarPanelItemConfiguration const* const* foobar_panel_configuration_get_items( + FoobarPanelConfiguration const* self, + gsize* out_count ) +{ + g_return_val_if_fail( self != NULL, NULL ); + + if ( out_count ) { *out_count = self->items_count; } + return (FoobarPanelItemConfiguration const* const*)self->items; +} + +// +// Mutable items configured to be displayed in the panel. +// +FoobarPanelItemConfiguration* const* foobar_panel_configuration_get_items_mut( + FoobarPanelConfiguration* self, + gsize* out_count ) +{ + g_return_val_if_fail( self != NULL, NULL ); + + if ( out_count ) { *out_count = self->items_count; } + return self->items; +} + +// +// The screen edge which the panel should appear on. +// +void foobar_panel_configuration_set_position( + FoobarPanelConfiguration* self, + FoobarScreenEdge value ) +{ + g_return_if_fail( self != NULL ); + self->position = value; +} + +// +// Offset of the panel from all screen edges. +// +void foobar_panel_configuration_set_margin( + FoobarPanelConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->margin = value; +} + +// +// Inset of the panel along its orientation. +// +void foobar_panel_configuration_set_padding( + FoobarPanelConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->padding = value; +} + +// +// Size of the panel (depending on the orientation, this can be either the width or the height). +// +void foobar_panel_configuration_set_size( + FoobarPanelConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->size = value; +} + +// +// Spacing between panel items. +// +void foobar_panel_configuration_set_spacing( + FoobarPanelConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->spacing = value; +} + +// +// Flag to enable the panel on all monitors. +// +void foobar_panel_configuration_set_multi_monitor( + FoobarPanelConfiguration* self, + gboolean value ) +{ + g_return_if_fail( self != NULL ); + self->multi_monitor = value; +} + +// +// Items configured to be displayed in the panel. +// +void foobar_panel_configuration_set_items( + FoobarPanelConfiguration* self, + FoobarPanelItemConfiguration const* const* value, + gsize value_count ) +{ + g_return_if_fail( self != NULL ); + g_return_if_fail( value != NULL || value_count == 0 ); + + for ( gsize i = 0; i < self->items_count; ++i ) + { + foobar_panel_item_configuration_free( self->items[i] ); + } + g_free( self->items ); + self->items = g_new0( FoobarPanelItemConfiguration*, value_count + 1 ); + self->items_count = 0; + for ( gsize i = 0; i < value_count; ++i ) + { + g_warn_if_fail( value[i] != NULL ); + if ( value[i] ) + { + self->items[self->items_count++] = foobar_panel_item_configuration_copy( value[i] ); + } + } +} + +// +// Populate a panel configuration structure from the "panel" section of a keyfile. +// +void foobar_panel_configuration_load( + FoobarPanelConfiguration* self, + GKeyFile* file ) +{ + gint position; + if ( try_get_enum_value( file, "panel", "position", FOOBAR_TYPE_SCREEN_EDGE, VALIDATE_NONE, &position ) ) + { + foobar_panel_configuration_set_position( self, position ); + } + + gint margin; + if ( try_get_int_value( file, "panel", "margin", VALIDATE_NON_NEGATIVE, &margin ) ) + { + foobar_panel_configuration_set_margin( self, margin ); + } + + gint padding; + if ( try_get_int_value( file, "panel", "padding", VALIDATE_NON_NEGATIVE, &padding ) ) + { + foobar_panel_configuration_set_padding( self, padding ); + } + + gint size; + if ( try_get_int_value( file, "panel", "size", VALIDATE_POSITIVE, &size ) ) + { + foobar_panel_configuration_set_size( self, size ); + } + + gint spacing; + if ( try_get_int_value( file, "panel", "spacing", VALIDATE_NON_NEGATIVE, &spacing ) ) + { + foobar_panel_configuration_set_spacing( self, spacing ); + } + + gboolean multi_monitor; + if ( try_get_boolean_value( file, "panel", "multi-monitor", VALIDATE_NONE, &multi_monitor ) ) + { + foobar_panel_configuration_set_multi_monitor( self, multi_monitor ); + } + + g_autoptr( GPtrArray ) items = g_ptr_array_new_with_free_func( (GDestroyNotify)foobar_panel_item_configuration_free ); + g_auto( GStrv ) groups = g_key_file_get_groups( file, NULL ); + for ( gchar** it = groups; *it; ++it ) + { + FoobarPanelItemConfiguration* item = foobar_panel_item_configuration_load( file, *it ); + if ( item ) { g_ptr_array_add( items, item ); } + } + foobar_panel_configuration_set_items( self, (FoobarPanelItemConfiguration const* const*)items->pdata, items->len ); +} + +// +// Store a panel configuration structure in the "panel" section of a keyfile. +// +void foobar_panel_configuration_store( + FoobarPanelConfiguration const* self, + GKeyFile* file ) +{ + FoobarScreenEdge position = foobar_panel_configuration_get_position( self ); + g_key_file_set_string( file, "panel", "position", enum_value_to_string( FOOBAR_TYPE_SCREEN_EDGE, position ) ); + g_key_file_set_comment( + file, + "panel", + "position", + " The screen edge which the panel should appear on.", + NULL ); + + gint margin = foobar_panel_configuration_get_margin( self ); + g_key_file_set_integer( file, "panel", "margin", margin ); + g_key_file_set_comment( + file, + "panel", + "margin", + " Offset of the panel from all screen edges.", + NULL ); + + gint padding = foobar_panel_configuration_get_padding( self ); + g_key_file_set_integer( file, "panel", "padding", padding ); + g_key_file_set_comment( + file, + "panel", + "padding", + " Inset of the panel along its orientation.", + NULL ); + + gint size = foobar_panel_configuration_get_size( self ); + g_key_file_set_integer( file, "panel", "size", size ); + g_key_file_set_comment( + file, + "panel", + "size", + " Size of the panel (depending on the orientation, this can be either the width or the height).", + NULL ); + + gint spacing = foobar_panel_configuration_get_spacing( self ); + g_key_file_set_integer( file, "panel", "spacing", spacing ); + g_key_file_set_comment( + file, + "panel", + "spacing", + " Spacing between panel items.", + NULL ); + + gboolean multi_monitor = foobar_panel_configuration_get_multi_monitor( self ); + g_key_file_set_boolean( file, "panel", "multi-monitor", multi_monitor ); + g_key_file_set_comment( + file, + "panel", + "multi-monitor", + " Flag to enable the panel on all monitors.", + NULL ); + + gsize items_count; + FoobarPanelItemConfiguration const* const* items = foobar_panel_configuration_get_items( self, &items_count ); + for ( gsize i = 0; i < items_count; ++i ) + { + foobar_panel_item_configuration_store( items[i], file ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Launcher Configuration +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a default launcher configuration structure. +// +FoobarLauncherConfiguration* foobar_launcher_configuration_new( void ) +{ + return foobar_launcher_configuration_copy( &default_launcher_configuration ); +} + +// +// Create a mutable copy of another launcher configuration structure. +// +FoobarLauncherConfiguration* foobar_launcher_configuration_copy( FoobarLauncherConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + + FoobarLauncherConfiguration* copy = g_new0( FoobarLauncherConfiguration, 1 ); + copy->width = self->width; + copy->position = self->position; + copy->max_height = self->max_height; + return copy; +} + +// +// Release resources associated with a launcher configuration structure. +// +void foobar_launcher_configuration_free( FoobarLauncherConfiguration* self ) +{ + g_return_if_fail( self != NULL ); + + g_free( self ); +} + +// +// Check if two launcher configuration structures are equal. +// +gboolean foobar_launcher_configuration_equal( + FoobarLauncherConfiguration const* a, + FoobarLauncherConfiguration const* b ) +{ + g_return_val_if_fail( a != NULL, FALSE ); + g_return_val_if_fail( b != NULL, FALSE ); + + if ( a->width != b->width ) { return FALSE; } + if ( a->position != b->position ) { return FALSE; } + if ( a->max_height != b->max_height ) { return FALSE; } + + return TRUE; +} + +// +// Horizontal size of the launcher. +// +gint foobar_launcher_configuration_get_width( FoobarLauncherConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->width; +} + +// +// Offset from the top of the screen. +// +gint foobar_launcher_configuration_get_position( FoobarLauncherConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->position; +} + +// +// Maximum allowed height for the launcher before scrolling is enabled. +// +gint foobar_launcher_configuration_get_max_height( FoobarLauncherConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->max_height; +} + +// +// Horizontal size of the launcher. +// +void foobar_launcher_configuration_set_width( + FoobarLauncherConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->width = value; +} + +// +// Offset from the top of the screen. +// +void foobar_launcher_configuration_set_position( + FoobarLauncherConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->position = value; +} + +// +// Maximum allowed height for the launcher before scrolling is enabled. +// +void foobar_launcher_configuration_set_max_height( + FoobarLauncherConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->max_height = value; +} + +// +// Populate a launcher configuration structure from the "launcher" section of a keyfile. +// +void foobar_launcher_configuration_load( + FoobarLauncherConfiguration* self, + GKeyFile* file ) +{ + gint width; + if ( try_get_int_value( file, "launcher", "width", VALIDATE_POSITIVE, &width ) ) + { + foobar_launcher_configuration_set_width( self, width ); + } + + gint position; + if ( try_get_int_value( file, "launcher", "position", VALIDATE_NON_NEGATIVE, &position ) ) + { + foobar_launcher_configuration_set_position( self, position ); + } + + gint max_height; + if ( try_get_int_value( file, "launcher", "max-height", VALIDATE_POSITIVE, &max_height ) ) + { + foobar_launcher_configuration_set_position( self, max_height ); + } +} + +// +// Store a launcher configuration structure in the "launcher" section of a keyfile. +// +void foobar_launcher_configuration_store( + FoobarLauncherConfiguration const* self, + GKeyFile* file ) +{ + gint width = foobar_launcher_configuration_get_width( self ); + g_key_file_set_integer( file, "launcher", "width", width ); + + gint position = foobar_launcher_configuration_get_position( self ); + g_key_file_set_integer( file, "launcher", "position", position ); + g_key_file_set_comment( + file, + "launcher", + "position", + " Offset from the top of the screen.", + NULL ); + + gint max_height = foobar_launcher_configuration_get_max_height( self ); + g_key_file_set_integer( file, "launcher", "max-height", max_height ); + g_key_file_set_comment( + file, + "launcher", + "max-height", + " Maximum allowed height for the launcher before scrolling is enabled.", + NULL ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Control Center Configuration +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a default control center configuration structure. +// +FoobarControlCenterConfiguration* foobar_control_center_configuration_new( void ) +{ + return foobar_control_center_configuration_copy( &default_control_center_configuration ); +} + +// +// Create a mutable copy of another control center configuration structure. +// +FoobarControlCenterConfiguration* foobar_control_center_configuration_copy( FoobarControlCenterConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + + FoobarControlCenterConfiguration* copy = g_new0( FoobarControlCenterConfiguration, 1 ); + copy->width = self->width; + copy->height = self->height; + copy->position = self->position; + copy->offset = self->offset; + copy->padding = self->padding; + copy->spacing = self->spacing; + copy->orientation = self->orientation; + copy->alignment = self->alignment; + copy->rows = g_new( FoobarControlCenterRow, self->rows_count ); + copy->rows_count = self->rows_count; + memcpy( copy->rows, self->rows, sizeof( *copy->rows ) * copy->rows_count ); + return copy; +} + +// +// Release resources associated with a control center configuration structure. +// +void foobar_control_center_configuration_free( FoobarControlCenterConfiguration* self ) +{ + g_return_if_fail( self != NULL ); + + g_free( self->rows ); + g_free( self ); +} + +// +// Check if two control center configuration structures are equal. +// +gboolean foobar_control_center_configuration_equal( + FoobarControlCenterConfiguration const* a, + FoobarControlCenterConfiguration const* b ) +{ + g_return_val_if_fail( a != NULL, FALSE ); + g_return_val_if_fail( b != NULL, FALSE ); + + if ( a->width != b->width ) { return FALSE; } + if ( a->height != b->height ) { return FALSE; } + if ( a->position != b->position ) { return FALSE; } + if ( a->offset != b->offset ) { return FALSE; } + if ( a->padding != b->padding ) { return FALSE; } + if ( a->spacing != b->spacing ) { return FALSE; } + if ( a->orientation != b->orientation ) { return FALSE; } + if ( a->alignment != b->alignment ) { return FALSE; } + if ( a->rows_count != b->rows_count ) { return FALSE; } + if ( memcmp( a->rows, b->rows, sizeof( *a->rows ) * a->rows_count ) ) { return FALSE; } + + return TRUE; +} + +// +// Horizontal size of the control center. +// +gint foobar_control_center_configuration_get_width( FoobarControlCenterConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->width; +} + +// +// Vertical size of the control center. +// +gint foobar_control_center_configuration_get_height( FoobarControlCenterConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->height; +} + +// +// The screen edge which the control center should be attached to. +// +FoobarScreenEdge foobar_control_center_configuration_get_position( FoobarControlCenterConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->position; +} + +// +// Offset from the attached screen edge. +// +gint foobar_control_center_configuration_get_offset( FoobarControlCenterConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->offset; +} + +// +// Spacing between the edges of the control center and its items. +// +gint foobar_control_center_configuration_get_padding( FoobarControlCenterConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->padding; +} + +// +// Spacing between the items. +// +gint foobar_control_center_configuration_get_spacing( FoobarControlCenterConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->spacing; +} + +// +// The orientation used to arrange the controls and notifications sections. +// +FoobarOrientation foobar_control_center_configuration_get_orientation( FoobarControlCenterConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->orientation; +} + +// +// Alignment along the attached screen edge, or "fill". +// +FoobarControlCenterAlignment foobar_control_center_configuration_get_alignment( FoobarControlCenterConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->alignment; +} + +// +// Ordered list of rows to display in the controls section. +// +FoobarControlCenterRow const* foobar_control_center_configuration_get_rows( + FoobarControlCenterConfiguration const* self, + gsize* out_count ) +{ + g_return_val_if_fail( self != NULL, 0 ); + g_return_val_if_fail( out_count != NULL, NULL ); + + *out_count = self->rows_count; + return self->rows; +} + +// +// Horizontal size of the control center. +// +void foobar_control_center_configuration_set_width( + FoobarControlCenterConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->width = value; +} + +// +// Vertical size of the control center. +// +void foobar_control_center_configuration_set_height( + FoobarControlCenterConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->height = value; +} + +// +// The screen edge which the control center should be attached to. +// +void foobar_control_center_configuration_set_position( + FoobarControlCenterConfiguration* self, + FoobarScreenEdge value ) +{ + g_return_if_fail( self != NULL ); + self->position = value; +} + +// +// Offset from the attached screen edge. +// +void foobar_control_center_configuration_set_offset( + FoobarControlCenterConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->offset = value; +} + +// +// Spacing between the edges of the control center and its items. +// +void foobar_control_center_configuration_set_padding( + FoobarControlCenterConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->padding = value; +} + +// +// Spacing between the items. +// +void foobar_control_center_configuration_set_spacing( + FoobarControlCenterConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->spacing = value; +} + +// +// The orientation used to arrange the controls and notifications sections. +// +void foobar_control_center_configuration_set_orientation( + FoobarControlCenterConfiguration* self, + FoobarOrientation value ) +{ + g_return_if_fail( self != NULL ); + self->orientation = value; +} + +// +// Alignment along the attached screen edge, or "fill". +// +void foobar_control_center_configuration_set_alignment( + FoobarControlCenterConfiguration* self, + FoobarControlCenterAlignment value ) +{ + g_return_if_fail( self != NULL ); + self->alignment = value; +} + +// +// Ordered list of rows to display in the controls section. +// +void foobar_control_center_configuration_set_rows( + FoobarControlCenterConfiguration* self, + FoobarControlCenterRow const* value, + gsize value_count ) +{ + g_return_if_fail( self != NULL ); + g_return_if_fail( value != NULL || value_count == 0 ); + + g_free( self->rows ); + self->rows = g_new( FoobarControlCenterRow, value_count ); + self->rows_count = value_count; + memcpy( self->rows, value, sizeof( *self->rows ) * self->rows_count ); +} + +// +// Populate a control center configuration structure from the "control-center" section of a keyfile. +// +void foobar_control_center_configuration_load( + FoobarControlCenterConfiguration* self, + GKeyFile* file ) +{ + gint width; + if ( try_get_int_value( file, "control-center", "width", VALIDATE_POSITIVE, &width ) ) + { + foobar_control_center_configuration_set_width( self, width ); + } + + gint height; + if ( try_get_int_value( file, "control-center", "height", VALIDATE_POSITIVE, &height ) ) + { + foobar_control_center_configuration_set_height( self, height ); + } + + gint position; + if ( try_get_enum_value( file, "control-center", "position", FOOBAR_TYPE_SCREEN_EDGE, VALIDATE_NONE, &position ) ) + { + foobar_control_center_configuration_set_position( self, position ); + } + + gint offset; + if ( try_get_int_value( file, "control-center", "offset", VALIDATE_NON_NEGATIVE, &offset ) ) + { + foobar_control_center_configuration_set_offset( self, offset ); + } + + gint padding; + if ( try_get_int_value( file, "control-center", "padding", VALIDATE_NON_NEGATIVE, &padding ) ) + { + foobar_control_center_configuration_set_padding( self, padding ); + } + + gint spacing; + if ( try_get_int_value( file, "control-center", "spacing", VALIDATE_NON_NEGATIVE, &spacing ) ) + { + foobar_control_center_configuration_set_spacing( self, spacing ); + } + + gint orientation; + if ( try_get_enum_value( file, "control-center", "orientation", FOOBAR_TYPE_ORIENTATION, VALIDATE_NONE, &orientation ) ) + { + foobar_control_center_configuration_set_orientation( self, orientation ); + } + + gint alignment; + if ( try_get_enum_value( file, "control-center", "alignment", FOOBAR_TYPE_CONTROL_CENTER_ALIGNMENT, VALIDATE_NONE, &alignment ) ) + { + foobar_control_center_configuration_set_alignment( self, alignment ); + } + + g_autofree gpointer rows = NULL; + gsize rows_count; + if ( try_get_enum_list_value( file, "control-center", "rows", FOOBAR_TYPE_CONTROL_CENTER_ROW, VALIDATE_DISTINCT, &rows, &rows_count ) ) + { + foobar_control_center_configuration_set_rows( self, rows, rows_count ); + } +} + +// +// Store a control center configuration structure in the "control-center" section of a keyfile. +// +void foobar_control_center_configuration_store( + FoobarControlCenterConfiguration const* self, + GKeyFile* file ) +{ + gint width = foobar_control_center_configuration_get_width( self ); + g_key_file_set_integer( file, "control-center", "width", width ); + + gint height = foobar_control_center_configuration_get_height( self ); + g_key_file_set_integer( file, "control-center", "height", height ); + + FoobarScreenEdge position = foobar_control_center_configuration_get_position( self ); + g_key_file_set_string( file, "control-center", "position", enum_value_to_string( FOOBAR_TYPE_SCREEN_EDGE, position ) ); + g_key_file_set_comment( + file, + "control-center", + "position", + " The screen edge which the control center should be attached to.", + NULL ); + + gint offset = foobar_control_center_configuration_get_offset( self ); + g_key_file_set_integer( file, "control-center", "offset", offset ); + g_key_file_set_comment( + file, + "control-center", + "offset", + " Offset from the attached screen edge.", + NULL ); + + gint padding = foobar_control_center_configuration_get_padding( self ); + g_key_file_set_integer( file, "control-center", "padding", padding ); + g_key_file_set_comment( + file, + "control-center", + "padding", + " Spacing between the edges of the control center and its items.", + NULL ); + + gint spacing = foobar_control_center_configuration_get_spacing( self ); + g_key_file_set_integer( file, "control-center", "spacing", spacing ); + g_key_file_set_comment( + file, + "control-center", + "spacing", + " Spacing between the items.", + NULL ); + + FoobarOrientation orientation = foobar_control_center_configuration_get_orientation( self ); + g_key_file_set_string( file, "control-center", "orientation", enum_value_to_string( FOOBAR_TYPE_ORIENTATION, orientation ) ); + g_key_file_set_comment( + file, + "control-center", + "orientation", + " The orientation used to arrange the controls and notifications sections.", + NULL ); + + FoobarControlCenterAlignment alignment = foobar_control_center_configuration_get_alignment( self ); + g_key_file_set_string( file, "control-center", "alignment", enum_value_to_string( FOOBAR_TYPE_CONTROL_CENTER_ALIGNMENT, alignment ) ); + g_key_file_set_comment( + file, + "control-center", + "alignment", + " Alignment along the attached screen edge, or \"fill\".", + NULL ); + + gsize rows_count; + FoobarControlCenterRow const* rows = foobar_control_center_configuration_get_rows( self, &rows_count ); + g_autofree gchar const** rows_str = enum_list_to_string( FOOBAR_TYPE_CONTROL_CENTER_ROW, rows, rows_count ); + g_key_file_set_string_list( file, "control-center", "rows", rows_str, rows_count ); + g_key_file_set_comment( + file, + "control-center", + "rows", + " Ordered list of rows to display in the controls section.", + NULL ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Notification Configuration +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a default notification configuration structure. +// +FoobarNotificationConfiguration* foobar_notification_configuration_new( void ) +{ + return foobar_notification_configuration_copy( &default_notification_configuration ); +} + +// +// Create a mutable copy of another notification configuration structure. +// +FoobarNotificationConfiguration* foobar_notification_configuration_copy( FoobarNotificationConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + + FoobarNotificationConfiguration* copy = g_new0( FoobarNotificationConfiguration, 1 ); + copy->width = self->width; + copy->min_height = self->min_height; + copy->spacing = self->spacing; + copy->close_button_inset = self->close_button_inset; + copy->time_format = g_strdup( self->time_format ); + return copy; +} + +// +// Release resources associated with a notification configuration structure. +// +void foobar_notification_configuration_free( FoobarNotificationConfiguration* self ) +{ + g_return_if_fail( self != NULL ); + + g_free( self->time_format ); + g_free( self ); +} + +// +// Check if two notification configuration structures are equal. +// +gboolean foobar_notification_configuration_equal( + FoobarNotificationConfiguration const* a, + FoobarNotificationConfiguration const* b ) +{ + g_return_val_if_fail( a != NULL, FALSE ); + g_return_val_if_fail( b != NULL, FALSE ); + + if ( a->width != b->width ) { return FALSE; } + if ( a->min_height != b->min_height ) { return FALSE; } + if ( a->spacing != b->spacing ) { return FALSE; } + if ( a->close_button_inset != b->close_button_inset ) { return FALSE; } + if ( g_strcmp0( a->time_format, b->time_format ) ) { return FALSE; } + + return TRUE; +} + +// +// Horizontal size of notifications in the notification area. +// +gint foobar_notification_configuration_get_width( FoobarNotificationConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->width; +} + +// +// Minimum height for each notification. +// +gint foobar_notification_configuration_get_min_height( FoobarNotificationConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->min_height; +} + +// +// Spacing between notifications and from the screen edges. +// +gint foobar_notification_configuration_get_spacing( FoobarNotificationConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->spacing; +} + +// +// Inset of the close button within the notification (may be negative). +// +gint foobar_notification_configuration_get_close_button_inset( FoobarNotificationConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, 0 ); + return self->close_button_inset; +} + +// +// The time format string as used by g_date_time_format. +// +gchar const* foobar_notification_configuration_get_time_format( FoobarNotificationConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + return self->time_format; +} + +// +// Horizontal size of notifications in the notification area. +// +void foobar_notification_configuration_set_width( + FoobarNotificationConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->width = value; +} + +// +// Minimum height for each notification. +// +void foobar_notification_configuration_set_min_height( + FoobarNotificationConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->min_height = value; +} + +// +// Spacing between notifications and from the screen edges. +// +void foobar_notification_configuration_set_spacing( + FoobarNotificationConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->spacing = value; +} + +// +// Inset of the close button within the notification (may be negative). +// +void foobar_notification_configuration_set_close_button_inset( + FoobarNotificationConfiguration* self, + gint value ) +{ + g_return_if_fail( self != NULL ); + self->close_button_inset = value; +} + +// +// The time format string as used by g_date_time_format. +// +void foobar_notification_configuration_set_time_format( + FoobarNotificationConfiguration* self, + gchar const* value ) +{ + g_return_if_fail( self != NULL ); + g_return_if_fail( value != NULL ); + + g_free( self->time_format ); + self->time_format = g_strdup( value ); +} + +// +// Populate a notification configuration structure from the "notifications" section of a keyfile. +// +void foobar_notification_configuration_load( + FoobarNotificationConfiguration* self, + GKeyFile* file ) +{ + gint width; + if ( try_get_int_value( file, "notifications", "width", VALIDATE_POSITIVE, &width ) ) + { + foobar_notification_configuration_set_width( self, width ); + } + + gint min_height; + if ( try_get_int_value( file, "notifications", "min-height", VALIDATE_NON_NEGATIVE, &min_height ) ) + { + foobar_notification_configuration_set_min_height( self, min_height ); + } + + gint spacing; + if ( try_get_int_value( file, "notifications", "spacing", VALIDATE_NON_NEGATIVE, &spacing ) ) + { + foobar_notification_configuration_set_spacing( self, spacing ); + } + + gint close_button_inset; + if ( try_get_int_value( file, "notifications", "close-button-inset", VALIDATE_NONE, &close_button_inset ) ) + { + foobar_notification_configuration_set_close_button_inset( self, close_button_inset ); + } + + g_autofree gchar* time_format = NULL; + if ( try_get_string_value( file, "notifications", "time-format", VALIDATE_NONE, &time_format ) ) + { + foobar_notification_configuration_set_time_format( self, time_format ); + } +} + +// +// Store a notification configuration structure in the "notifications" section of a keyfile. +// +void foobar_notification_configuration_store( + FoobarNotificationConfiguration const* self, + GKeyFile* file ) +{ + gint width = foobar_notification_configuration_get_width( self ); + g_key_file_set_integer( file, "notifications", "width", width ); + + gint min_height = foobar_notification_configuration_get_min_height( self ); + g_key_file_set_integer( file, "notifications", "min-height", min_height ); + g_key_file_set_comment( + file, + "notifications", + "min-height", + " Minimum height for each notification.", + NULL ); + + gint spacing = foobar_notification_configuration_get_spacing( self ); + g_key_file_set_integer( file, "notifications", "spacing", spacing ); + g_key_file_set_comment( + file, + "notifications", + "spacing", + " Spacing between notifications and from the screen edges.", + NULL ); + + gint close_button_inset = foobar_notification_configuration_get_close_button_inset( self ); + g_key_file_set_integer( file, "notifications", "close-button-inset", close_button_inset ); + g_key_file_set_comment( + file, + "notifications", + "close-button-inset", + " Inset of the close button within the notification (may be negative).", + NULL ); + + gchar const* time_format = foobar_notification_configuration_get_time_format( self ); + g_key_file_set_string( file, "notifications", "time-format", time_format ); + g_key_file_set_comment( + file, + "notifications", + "time-format", + " The time format string as used by g_date_time_format.", + NULL ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a default configuration structure. +// +FoobarConfiguration* foobar_configuration_new( void ) +{ + return foobar_configuration_copy( &default_configuration ); +} + +// +// Create a mutable copy of another configuration structure. +// +FoobarConfiguration* foobar_configuration_copy( FoobarConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + + FoobarConfiguration* copy = g_new0( FoobarConfiguration, 1 ); + copy->general = foobar_general_configuration_copy( self->general ); + copy->panel = foobar_panel_configuration_copy( self->panel ); + copy->launcher = foobar_launcher_configuration_copy( self->launcher ); + copy->control_center = foobar_control_center_configuration_copy( self->control_center ); + copy->notifications = foobar_notification_configuration_copy( self->notifications ); + return copy; +} + +// +// Release resources associated with a configuration structure. +// +void foobar_configuration_free( FoobarConfiguration* self ) +{ + g_return_if_fail( self != NULL ); + + foobar_general_configuration_free( self->general ); + foobar_panel_configuration_free( self->panel ); + foobar_launcher_configuration_free( self->launcher ); + foobar_control_center_configuration_free( self->control_center ); + foobar_notification_configuration_free( self->notifications ); + g_free( self ); +} + +// +// Check if two configuration structures are equal. +// +gboolean foobar_configuration_equal( + FoobarConfiguration const* a, + FoobarConfiguration const* b ) +{ + g_return_val_if_fail( a != NULL, FALSE ); + g_return_val_if_fail( b != NULL, FALSE ); + + if ( !foobar_general_configuration_equal( a->general, b->general ) ) { return FALSE; } + if ( !foobar_panel_configuration_equal( a->panel, b->panel ) ) { return FALSE; } + if ( !foobar_launcher_configuration_equal( a->launcher, b->launcher ) ) { return FALSE; } + if ( !foobar_control_center_configuration_equal( a->control_center, b->control_center ) ) { return FALSE; } + if ( !foobar_notification_configuration_equal( a->notifications, b->notifications ) ) { return FALSE; } + + return TRUE; +} + +// +// Get the general application configuration. +// +FoobarGeneralConfiguration const* foobar_configuration_get_general( FoobarConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + return self->general; +} + +// +// Get a mutable reference to the general application configuration. +// +FoobarGeneralConfiguration* foobar_configuration_get_general_mut( FoobarConfiguration* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + return self->general; +} + +// +// Get the panel configuration. +// +FoobarPanelConfiguration const* foobar_configuration_get_panel( FoobarConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + return self->panel; +} + +// +// Get a mutable reference to the panel configuration. +// +FoobarPanelConfiguration* foobar_configuration_get_panel_mut( FoobarConfiguration* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + return self->panel; +} + +// +// Get the launcher configuration. +// +FoobarLauncherConfiguration const* foobar_configuration_get_launcher( FoobarConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + return self->launcher; +} + +// +// Get a mutable reference to the launcher configuration. +// +FoobarLauncherConfiguration* foobar_configuration_get_launcher_mut( FoobarConfiguration* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + return self->launcher; +} + +// +// Get the control center configuration. +// +FoobarControlCenterConfiguration const* foobar_configuration_get_control_center( FoobarConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + return self->control_center; +} + +// +// Get a mutable reference to the control center configuration. +// +FoobarControlCenterConfiguration* foobar_configuration_get_control_center_mut( FoobarConfiguration* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + return self->control_center; +} + +// +// Get the notification configuration. +// +FoobarNotificationConfiguration const* foobar_configuration_get_notifications( FoobarConfiguration const* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + return self->notifications; +} + +// +// Get a mutable reference to the notification configuration. +// +FoobarNotificationConfiguration* foobar_configuration_get_notifications_mut( FoobarConfiguration* self ) +{ + g_return_val_if_fail( self != NULL, NULL ); + return self->notifications; +} + +// +// Populate a configuration structure from a keyfile. +// +void foobar_configuration_load( + FoobarConfiguration* self, + GKeyFile* file ) +{ + foobar_general_configuration_load( foobar_configuration_get_general_mut( self ), file ); + foobar_panel_configuration_load( foobar_configuration_get_panel_mut( self ), file ); + foobar_launcher_configuration_load( foobar_configuration_get_launcher_mut( self ), file ); + foobar_control_center_configuration_load( foobar_configuration_get_control_center_mut( self ), file ); + foobar_notification_configuration_load( foobar_configuration_get_notifications_mut( self ), file ); +} + +// +// Store a configuration structure in a keyfile. +// +void foobar_configuration_store( + FoobarConfiguration const* self, + GKeyFile* file ) +{ + foobar_general_configuration_store( foobar_configuration_get_general( self ), file ); + foobar_panel_configuration_store( foobar_configuration_get_panel( self ), file ); + foobar_launcher_configuration_store( foobar_configuration_get_launcher( self ), file ); + foobar_control_center_configuration_store( foobar_configuration_get_control_center( self ), file ); + foobar_notification_configuration_store( foobar_configuration_get_notifications( self ), file ); +} + +// +// Populate a configuration structure from a keyfile at the given path. +// +void foobar_configuration_load_from_file( + FoobarConfiguration* self, + gchar const* path ) +{ + g_autoptr( GKeyFile ) file = g_key_file_new( ); + g_autoptr( GError ) error = NULL; + if ( !g_key_file_load_from_file( file, path, G_KEY_FILE_NONE, &error ) ) + { + CONFIGURATION_WARNING( "%s", error->message ); + return; + } + foobar_configuration_load( self, file ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the configuration service. +// +void foobar_configuration_service_class_init( FoobarConfigurationServiceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_configuration_service_get_property; + object_klass->finalize = foobar_configuration_service_finalize; + + props[PROP_CURRENT] = g_param_spec_boxed( + "current", + "Current", + "The current configuration state.", + FOOBAR_TYPE_CONFIGURATION, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for the configuration service. +// +void foobar_configuration_service_init( FoobarConfigurationService* self ) +{ + self->path = g_strdup_printf( "%s/foobar.conf", g_get_user_config_dir( ) ); + self->current = foobar_configuration_new( ); + g_return_if_fail( self->path != NULL ); + + // Ensure that the file at path exists. + + if ( g_file_test( self->path, G_FILE_TEST_EXISTS ) ) + { + foobar_configuration_load_from_file( self->current, self->path ); + } + else + { + g_info( "Config file does not exist -- falling back to default config and creating a copy of it." ); + g_autoptr( GKeyFile ) file = g_key_file_new( ); + foobar_configuration_store( self->current, file ); + g_key_file_save_to_file( file, self->path, NULL ); + } + + // Monitor the config file. + + self->monitor = foobar_configuration_service_create_monitor( self->path ); + if ( self->monitor ) + { + self->changed_handler_id = g_signal_connect( + self->monitor, + "changed", + G_CALLBACK( foobar_configuration_service_handle_changed ), + self ); + } + + // In case the config is symlinked, also watch the actual file for content changes. + + self->actual_path = realpath( self->path, NULL ); + if ( self->actual_path && g_strcmp0( self->actual_path, self->path ) ) + { + self->actual_monitor = foobar_configuration_service_create_monitor( self->actual_path ); + if ( self->actual_monitor ) + { + self->actual_changed_handler_id = g_signal_connect( + self->actual_monitor, + "changed", + G_CALLBACK( foobar_configuration_service_handle_actual_changed ), + self ); + } + } +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_configuration_service_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarConfigurationService* self = (FoobarConfigurationService*)object; + + switch ( prop_id ) + { + case PROP_CURRENT: + g_value_set_boxed( value, foobar_configuration_service_get_current( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for the configuration service. +// +void foobar_configuration_service_finalize( GObject* object ) +{ + FoobarConfigurationService* self = (FoobarConfigurationService*)object; + + g_clear_handle_id( &self->update_source_id, g_source_remove ); + g_clear_signal_handler( &self->changed_handler_id, self->monitor ); + g_clear_signal_handler( &self->actual_changed_handler_id, self->actual_monitor ); + g_clear_object( &self->monitor ); + g_clear_object( &self->actual_monitor ); + g_clear_pointer( &self->current, foobar_configuration_free ); + g_clear_pointer( &self->path, g_free ); + g_clear_pointer( &self->actual_path, free ); + + G_OBJECT_CLASS( foobar_configuration_service_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new configuration service instance. +// +FoobarConfigurationService* foobar_configuration_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_CONFIGURATION_SERVICE, NULL ); +} + +// +// Get the current configuration state. +// +FoobarConfiguration const* foobar_configuration_service_get_current( FoobarConfigurationService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( self ), NULL ); + return self->current; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called when the file at self->path has changed. +// +void foobar_configuration_service_handle_changed( + GFileMonitor* monitor, + GFile* file, + GFile* other_file, + GFileMonitorEvent event_type, + gpointer userdata ) +{ + (void)monitor; + (void)file; + (void)other_file; + FoobarConfigurationService* self = (FoobarConfigurationService*)userdata; + + // If this file is a symlink, the actual path might have changed. + + char* actual_path = realpath( self->path, NULL ); + if ( g_strcmp0( self->actual_path, actual_path ) ) + { + free(self->actual_path); + self->actual_path = g_steal_pointer( &actual_path ); + + // Recreate the file monitor. + + g_clear_signal_handler( &self->actual_changed_handler_id, self->actual_monitor ); + g_clear_object( &self->actual_monitor ); + + if ( self->actual_path && g_strcmp0( self->actual_path, self->path ) ) + { + self->actual_monitor = foobar_configuration_service_create_monitor( self->actual_path ); + if ( self->actual_monitor ) + { + self->actual_changed_handler_id = g_signal_connect( + self->actual_monitor, + "changed", + G_CALLBACK( foobar_configuration_service_handle_actual_changed ), + self ); + } + } + } + free(actual_path); + + // Handle actual file changes. + + if ( event_type == G_FILE_MONITOR_EVENT_CREATED || event_type == G_FILE_MONITOR_EVENT_CHANGED ) + { + foobar_configuration_service_update( self ); + } +} + +// +// Called when the actual file (the one at self->actual_path) behind the symlink has changed. +// +void foobar_configuration_service_handle_actual_changed( + GFileMonitor* monitor, + GFile* file, + GFile* other_file, + GFileMonitorEvent event_type, + gpointer userdata ) +{ + (void)monitor; + (void)file; + (void)other_file; + FoobarConfigurationService* self = (FoobarConfigurationService*)userdata; + + if ( event_type == G_FILE_MONITOR_EVENT_CREATED || event_type == G_FILE_MONITOR_EVENT_CHANGED ) + { + foobar_configuration_service_update( self ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Schedule a configuration file reload, used to coalesce change events. +// +void foobar_configuration_service_update( FoobarConfigurationService* self ) +{ + if ( !self->update_source_id ) + { + self->update_source_id = g_timeout_add( + UPDATE_APPLY_DELAY, + foobar_configuration_service_update_cb, + self ); + } +} + +// +// Reload the configuration file. +// +gboolean foobar_configuration_service_update_cb( gpointer userdata ) +{ + FoobarConfigurationService* self = (FoobarConfigurationService*)userdata; + + FoobarConfiguration* updated = foobar_configuration_new( ); + foobar_configuration_load_from_file( updated, self->path ); + if ( !foobar_configuration_equal( self->current, updated ) ) + { + foobar_configuration_free( self->current ); + self->current = g_steal_pointer( &updated ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CURRENT] ); + g_info( "Config reloaded." ); + } + g_clear_pointer( &updated, foobar_configuration_free ); + + self->update_source_id = 0; + return G_SOURCE_REMOVE; +} + +// +// Create a new file monitor, logging a warning on error. +// +GFileMonitor* foobar_configuration_service_create_monitor( gchar const* path ) +{ + g_autoptr( GError ) error = NULL; + g_autoptr( GFile ) file = g_file_new_for_path( path ); + GFileMonitor* monitor = g_file_monitor( file, G_FILE_MONITOR_NONE, NULL, &error ); + if ( !monitor ) + { + g_warning( "Unable to monitor config file: %s", error->message ); + } + + return monitor; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Parsing Helpers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Get and validate a string value from a keyfile. *out should always be freed, even if FALSE is returned. +// +gboolean try_get_string_value( + GKeyFile* file, + gchar const* group, + gchar const* key, + ValidationFlags validation, + gchar** out ) +{ + if ( !g_key_file_has_key( file, group, key, NULL ) ) { return FALSE; } + + g_autoptr( GError ) error = NULL; + *out = g_key_file_get_string( file, group, key, &error ); + if ( !*out ) + { + CONFIGURATION_WARNING( "%s:%s: %s", group, key, error->message ); + return FALSE; + } + + if ( validation & VALIDATE_FILE_URL ) + { + g_autoptr( GFile ) test_file = g_file_new_for_uri( *out ); + if ( !g_file_query_exists( test_file, NULL ) ) + { + CONFIGURATION_WARNING( "%s:%s: A file with the URL \"%s\" does not exist.", group, key, *out ); + return FALSE; + } + } + + return TRUE; +} + +// +// Get and validate a list of string values from a keyfile. *out should always be freed, even if FALSE is returned. +// +gboolean try_get_string_list_value( + GKeyFile* file, + gchar const* group, + gchar const* key, + ValidationFlags validation, + GStrv* out, + gsize* out_count ) +{ + if ( !g_key_file_has_key( file, group, key, NULL ) ) { return FALSE; } + + g_autoptr( GError ) error = NULL; + *out = g_key_file_get_string_list( file, group, key, out_count, &error ); + if ( !*out ) + { + CONFIGURATION_WARNING( "%s:%s: %s", group, key, error->message ); + return FALSE; + } + + if ( validation & VALIDATE_DISTINCT ) + { + g_autoptr( GHashTable ) items = g_hash_table_new( g_str_hash, g_str_equal ); + for ( gchar** it = *out; *it; ++it ) + { + if ( !g_hash_table_add( items, *it ) ) + { + CONFIGURATION_WARNING( "%s:%s: Duplicate value \"%s\" not allowed.", group, key, *it ); + return FALSE; + } + } + } + + return TRUE; +} + +// +// Get and validate an integer value from a keyfile. +// +gboolean try_get_int_value( + GKeyFile* file, + gchar const* group, + gchar const* key, + ValidationFlags validation, + gint* out ) +{ + if ( !g_key_file_has_key( file, group, key, NULL ) ) { return FALSE; } + + g_autoptr( GError ) error = NULL; + *out = g_key_file_get_integer( file, group, key, &error ); + if ( error ) + { + CONFIGURATION_WARNING( "%s:%s: %s", group, key, error->message ); + return FALSE; + } + + if ( validation & VALIDATE_NON_NEGATIVE ) + { + if ( *out < 0 ) + { + CONFIGURATION_WARNING( "%s:%s: Value is not allowed to be negative.", group, key ); + return FALSE; + } + } + + if ( validation & VALIDATE_NON_ZERO ) + { + if ( *out == 0 ) + { + CONFIGURATION_WARNING( "%s:%s: Value is not allowed to be 0.", group, key ); + return FALSE; + } + } + + return TRUE; +} + +// +// Get and validate an enum value (encoded as a string) from a keyfile. +// +gboolean try_get_enum_value( + GKeyFile* file, + gchar const* group, + gchar const* key, + GType enum_type, + ValidationFlags validation, + gint* out ) +{ + (void)validation; + + g_autofree gchar* str = NULL; + if ( !try_get_string_value( file, group, key, VALIDATE_NONE, &str ) ) { return FALSE; } + + g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type ); + GEnumValue* val = g_enum_get_value_by_nick( enum_klass, str ); + if ( !val ) + { + g_autofree gchar* help_text = enum_format_help_text( enum_type ); + CONFIGURATION_WARNING( "%s:%s: Invalid enum value \"%s\" (allowed values: %s).", group, key, str, help_text ); + return FALSE; + } + + *out = val->value; + return TRUE; +} + +// +// Get and validate a list of enum values (encoded as strings) from a keyfile. *out should always be freed, even if +// FALSE is returned. +// +gboolean try_get_enum_list_value( + GKeyFile* file, + gchar const* group, + gchar const* key, + GType enum_type, + ValidationFlags validation, + gpointer* out, + gsize* out_count ) +{ + g_auto( GStrv ) str_values = NULL; + if ( !try_get_string_list_value( file, group, key, validation, &str_values, out_count ) ) { return FALSE; } + + gint* untyped_out = g_new0( gint, *out_count ); + *out = untyped_out; + + g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type ); + for ( gsize i = 0; i < *out_count; ++i ) + { + GEnumValue* val = g_enum_get_value_by_nick( enum_klass, str_values[i] ); + if ( !val ) + { + g_autofree gchar* help_text = enum_format_help_text( enum_type ); + CONFIGURATION_WARNING( "%s:%s: Invalid enum value \"%s\" (allowed values: %s).", group, key, str_values[i], help_text ); + return FALSE; + } + + untyped_out[i] = val->value; + } + + return TRUE; +} + +// +// Get and validate a boolean value from a keyfile. +// +gboolean try_get_boolean_value( + GKeyFile* file, + gchar const* group, + gchar const* key, + ValidationFlags validation, + gboolean* out ) +{ + (void)validation; + + if ( !g_key_file_has_key( file, group, key, NULL ) ) { return FALSE; } + + g_autoptr( GError ) error = NULL; + *out = g_key_file_get_boolean( file, group, key, &error ); + if ( error ) + { + CONFIGURATION_WARNING( "%s:%s: %s", group, key, error->message ); + return FALSE; + } + + return TRUE; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Serialization +// --------------------------------------------------------------------------------------------------------------------- + +// +// Get the string constant for an enum value. +// +gchar const* enum_value_to_string( + GType enum_type, + gint value ) +{ + g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type ); + GEnumValue* val = g_enum_get_value( enum_klass, value ); + return val->value_nick; +} + +// +// Get a list of string constants for a list of enum values. Only the list itself needs to be freed. +// +gchar const** enum_list_to_string( + GType enum_type, + gconstpointer values, + gsize values_count ) +{ + gint const* untyped_values = values; + gchar const** result = g_new0( gchar const*, values_count ); + + g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type ); + for ( gsize i = 0; i < values_count; ++i ) + { + GEnumValue* val = g_enum_get_value( enum_klass, untyped_values[i] ); + result[i] = val->value_nick; + } + + return result; +} + +// +// Return a comma-separated list of all allowed values for an enum type. The result needs to be freed. +// +gchar* enum_format_help_text( GType enum_type ) +{ + GString* result = g_string_new( NULL ); + g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type ); + + gboolean is_first = TRUE; + for ( guint i = 0; i < enum_klass->n_values; ++i ) + { + g_string_append_printf( result, is_first ? "\"%s\"" : ", \"%s\"", enum_klass->values[i].value_nick ); + is_first = FALSE; + } + + return g_string_free_and_steal( result ); +} diff --git a/csrc/services/configuration-service.h b/csrc/services/configuration-service.h new file mode 100644 index 0000000..2c1296b --- /dev/null +++ b/csrc/services/configuration-service.h @@ -0,0 +1,305 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_SCREEN_EDGE foobar_screen_edge_get_type( ) +#define FOOBAR_TYPE_ORIENTATION foobar_orientation_get_type( ) +#define FOOBAR_TYPE_STATUS_ITEM foobar_status_item_get_type( ) +#define FOOBAR_TYPE_PANEL_ITEM_KIND foobar_panel_item_kind_get_type( ) +#define FOOBAR_TYPE_PANEL_ITEM_ACTION foobar_panel_item_action_get_type( ) +#define FOOBAR_TYPE_PANEL_ITEM_POSITION foobar_panel_item_position_get_type( ) +#define FOOBAR_TYPE_CONTROL_CENTER_ROW foobar_control_center_row_get_type( ) +#define FOOBAR_TYPE_CONTROL_CENTER_ALIGNMENT foobar_control_center_alignment_get_type( ) +#define FOOBAR_TYPE_GENERAL_CONFIGURATION foobar_general_configuration_get_type( ) +#define FOOBAR_TYPE_PANEL_ITEM_CONFIGURATION foobar_panel_item_configuration_get_type( ) +#define FOOBAR_TYPE_PANEL_CONFIGURATION foobar_panel_configuration_get_type( ) +#define FOOBAR_TYPE_LAUNCHER_CONFIGURATION foobar_launcher_configuration_get_type( ) +#define FOOBAR_TYPE_CONTROL_CENTER_CONFIGURATION foobar_control_center_configuration_get_type( ) +#define FOOBAR_TYPE_NOTIFICATION_CONFIGURATION foobar_notification_configuration_get_type( ) +#define FOOBAR_TYPE_CONFIGURATION foobar_configuration_get_type( ) +#define FOOBAR_TYPE_CONFIGURATION_SERVICE foobar_configuration_service_get_type( ) + +typedef enum +{ + FOOBAR_SCREEN_EDGE_LEFT, + FOOBAR_SCREEN_EDGE_RIGHT, + FOOBAR_SCREEN_EDGE_TOP, + FOOBAR_SCREEN_EDGE_BOTTOM, +} FoobarScreenEdge; + +GType foobar_screen_edge_get_type( void ); + +typedef enum +{ + FOOBAR_ORIENTATION_HORIZONTAL, + FOOBAR_ORIENTATION_VERTICAL, +} FoobarOrientation; + +GType foobar_orientation_get_type( void ); + +typedef enum +{ + FOOBAR_STATUS_ITEM_NETWORK, + FOOBAR_STATUS_ITEM_BLUETOOTH, + FOOBAR_STATUS_ITEM_BATTERY, + FOOBAR_STATUS_ITEM_BRIGHTNESS, + FOOBAR_STATUS_ITEM_AUDIO, + FOOBAR_STATUS_ITEM_NOTIFICATIONS, +} FoobarStatusItem; + +GType foobar_status_item_get_type( void ); + +typedef enum +{ + FOOBAR_PANEL_ITEM_KIND_ICON, + FOOBAR_PANEL_ITEM_KIND_CLOCK, + FOOBAR_PANEL_ITEM_KIND_WORKSPACES, + FOOBAR_PANEL_ITEM_KIND_STATUS, +} FoobarPanelItemKind; + +GType foobar_panel_item_kind_get_type( void ); + +typedef enum +{ + FOOBAR_PANEL_ITEM_ACTION_NONE, + FOOBAR_PANEL_ITEM_ACTION_LAUNCHER, + FOOBAR_PANEL_ITEM_ACTION_CONTROL_CENTER, +} FoobarPanelItemAction; + +GType foobar_panel_item_action_get_type( void ); + +typedef enum +{ + FOOBAR_PANEL_ITEM_POSITION_START, + FOOBAR_PANEL_ITEM_POSITION_CENTER, + FOOBAR_PANEL_ITEM_POSITION_END, +} FoobarPanelItemPosition; + +GType foobar_panel_item_position_get_type( void ); + +typedef enum +{ + FOOBAR_CONTROL_CENTER_ROW_CONNECTIVITY, + FOOBAR_CONTROL_CENTER_ROW_AUDIO_OUTPUT, + FOOBAR_CONTROL_CENTER_ROW_AUDIO_INPUT, + FOOBAR_CONTROL_CENTER_ROW_BRIGHTNESS, +} FoobarControlCenterRow; + +GType foobar_control_center_row_get_type( void ); + +typedef enum +{ + FOOBAR_CONTROL_CENTER_ALIGNMENT_START, + FOOBAR_CONTROL_CENTER_ALIGNMENT_CENTER, + FOOBAR_CONTROL_CENTER_ALIGNMENT_END, + FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL, +} FoobarControlCenterAlignment; + +GType foobar_control_center_alignment_get_type( void ); + +typedef struct _FoobarGeneralConfiguration FoobarGeneralConfiguration; + +GType foobar_general_configuration_get_type ( void ); +FoobarGeneralConfiguration* foobar_general_configuration_new ( void ); +FoobarGeneralConfiguration* foobar_general_configuration_copy ( FoobarGeneralConfiguration const* self ); +void foobar_general_configuration_free ( FoobarGeneralConfiguration* self ); +gboolean foobar_general_configuration_equal ( FoobarGeneralConfiguration const* a, + FoobarGeneralConfiguration const* b ); +gchar const* foobar_general_configuration_get_stylesheet( FoobarGeneralConfiguration const* self ); +void foobar_general_configuration_set_stylesheet( FoobarGeneralConfiguration* self, + gchar const* value ); + +typedef struct _FoobarPanelItemConfiguration FoobarPanelItemConfiguration; + +GType foobar_panel_item_configuration_get_type ( void ); +FoobarPanelItemConfiguration* foobar_panel_item_configuration_copy ( FoobarPanelItemConfiguration const* self ); +void foobar_panel_item_configuration_free ( FoobarPanelItemConfiguration* self ); +gboolean foobar_panel_item_configuration_equal ( FoobarPanelItemConfiguration const* a, + FoobarPanelItemConfiguration const* b ); +FoobarPanelItemKind foobar_panel_item_configuration_get_kind ( FoobarPanelItemConfiguration const* self ); +gchar const* foobar_panel_item_configuration_get_name ( FoobarPanelItemConfiguration const* self ); +FoobarPanelItemPosition foobar_panel_item_configuration_get_position( FoobarPanelItemConfiguration const* self ); +FoobarPanelItemAction foobar_panel_item_configuration_get_action ( FoobarPanelItemConfiguration const* self ); +void foobar_panel_item_configuration_set_name ( FoobarPanelItemConfiguration* self, + gchar const* value ); +void foobar_panel_item_configuration_set_position( FoobarPanelItemConfiguration* self, + FoobarPanelItemPosition value ); +void foobar_panel_item_configuration_set_action ( FoobarPanelItemConfiguration* self, + FoobarPanelItemAction value ); + +FoobarPanelItemConfiguration* foobar_panel_item_icon_configuration_new ( void ); +gchar const* foobar_panel_item_icon_configuration_get_icon_name( FoobarPanelItemConfiguration const* self ); +void foobar_panel_item_icon_configuration_set_icon_name( FoobarPanelItemConfiguration* self, + gchar const* value ); + +FoobarPanelItemConfiguration* foobar_panel_item_clock_configuration_new ( void ); +gchar const* foobar_panel_item_clock_configuration_get_format( FoobarPanelItemConfiguration const* self ); +void foobar_panel_item_clock_configuration_set_format( FoobarPanelItemConfiguration* self, + gchar const* value ); + +FoobarPanelItemConfiguration* foobar_panel_item_workspaces_configuration_new ( void ); +gint foobar_panel_item_workspaces_configuration_get_button_size( FoobarPanelItemConfiguration const* self ); +gint foobar_panel_item_workspaces_configuration_get_spacing ( FoobarPanelItemConfiguration const* self ); +void foobar_panel_item_workspaces_configuration_set_button_size( FoobarPanelItemConfiguration* self, + gint value ); +void foobar_panel_item_workspaces_configuration_set_spacing ( FoobarPanelItemConfiguration* self, + gint value ); + +FoobarPanelItemConfiguration* foobar_panel_item_status_configuration_new ( void ); +FoobarStatusItem const* foobar_panel_item_status_configuration_get_items ( FoobarPanelItemConfiguration const* self, + gsize* out_count ); +gint foobar_panel_item_status_configuration_get_spacing ( FoobarPanelItemConfiguration const* self ); +gboolean foobar_panel_item_status_configuration_get_show_labels ( FoobarPanelItemConfiguration const* self ); +gboolean foobar_panel_item_status_configuration_get_enable_scrolling( FoobarPanelItemConfiguration const* self ); +void foobar_panel_item_status_configuration_set_items ( FoobarPanelItemConfiguration* self, + FoobarStatusItem const* value, + gsize value_count ); +void foobar_panel_item_status_configuration_set_spacing ( FoobarPanelItemConfiguration* self, + gint value ); +void foobar_panel_item_status_configuration_set_show_labels ( FoobarPanelItemConfiguration* self, + gboolean value ); +void foobar_panel_item_status_configuration_set_enable_scrolling( FoobarPanelItemConfiguration* self, + gboolean value ); + +typedef struct _FoobarPanelConfiguration FoobarPanelConfiguration; + +GType foobar_panel_configuration_get_type ( void ); +FoobarPanelConfiguration* foobar_panel_configuration_new ( void ); +FoobarPanelConfiguration* foobar_panel_configuration_copy ( FoobarPanelConfiguration const* self ); +void foobar_panel_configuration_free ( FoobarPanelConfiguration* self ); +gboolean foobar_panel_configuration_equal ( FoobarPanelConfiguration const* a, + FoobarPanelConfiguration const* b ); +FoobarScreenEdge foobar_panel_configuration_get_position ( FoobarPanelConfiguration const* self ); +gint foobar_panel_configuration_get_margin ( FoobarPanelConfiguration const* self ); +gint foobar_panel_configuration_get_padding ( FoobarPanelConfiguration const* self ); +gint foobar_panel_configuration_get_size ( FoobarPanelConfiguration const* self ); +gint foobar_panel_configuration_get_spacing ( FoobarPanelConfiguration const* self ); +gboolean foobar_panel_configuration_get_multi_monitor( FoobarPanelConfiguration const* self ); +FoobarPanelItemConfiguration const* const* foobar_panel_configuration_get_items ( FoobarPanelConfiguration const* self, + gsize* out_count ); +FoobarPanelItemConfiguration* const* foobar_panel_configuration_get_items_mut ( FoobarPanelConfiguration* self, + gsize* out_count ); +void foobar_panel_configuration_set_position ( FoobarPanelConfiguration* self, + FoobarScreenEdge value ); +void foobar_panel_configuration_set_margin ( FoobarPanelConfiguration* self, + gint value ); +void foobar_panel_configuration_set_padding ( FoobarPanelConfiguration* self, + gint value ); +void foobar_panel_configuration_set_size ( FoobarPanelConfiguration* self, + gint value ); +void foobar_panel_configuration_set_spacing ( FoobarPanelConfiguration* self, + gint value ); +void foobar_panel_configuration_set_multi_monitor( FoobarPanelConfiguration* self, + gboolean value ); +void foobar_panel_configuration_set_items ( FoobarPanelConfiguration* self, + FoobarPanelItemConfiguration const* const* value, + gsize value_count ); + +typedef struct _FoobarLauncherConfiguration FoobarLauncherConfiguration; + +GType foobar_launcher_configuration_get_type ( void ); +FoobarLauncherConfiguration* foobar_launcher_configuration_new ( void ); +FoobarLauncherConfiguration* foobar_launcher_configuration_copy ( FoobarLauncherConfiguration const* self ); +void foobar_launcher_configuration_free ( FoobarLauncherConfiguration* self ); +gboolean foobar_launcher_configuration_equal ( FoobarLauncherConfiguration const* a, + FoobarLauncherConfiguration const* b ); +gint foobar_launcher_configuration_get_width ( FoobarLauncherConfiguration const* self ); +gint foobar_launcher_configuration_get_position ( FoobarLauncherConfiguration const* self ); +gint foobar_launcher_configuration_get_max_height( FoobarLauncherConfiguration const* self ); +void foobar_launcher_configuration_set_width ( FoobarLauncherConfiguration* self, + gint value ); +void foobar_launcher_configuration_set_position ( FoobarLauncherConfiguration* self, + gint value ); +void foobar_launcher_configuration_set_max_height( FoobarLauncherConfiguration* self, + gint value ); + +typedef struct _FoobarControlCenterConfiguration FoobarControlCenterConfiguration; + +GType foobar_control_center_configuration_get_type ( void ); +FoobarControlCenterConfiguration* foobar_control_center_configuration_new ( void ); +FoobarControlCenterConfiguration* foobar_control_center_configuration_copy ( FoobarControlCenterConfiguration const* self ); +void foobar_control_center_configuration_free ( FoobarControlCenterConfiguration* self ); +gboolean foobar_control_center_configuration_equal ( FoobarControlCenterConfiguration const* a, + FoobarControlCenterConfiguration const* b ); +gint foobar_control_center_configuration_get_width ( FoobarControlCenterConfiguration const* self ); +gint foobar_control_center_configuration_get_height ( FoobarControlCenterConfiguration const* self ); +FoobarScreenEdge foobar_control_center_configuration_get_position ( FoobarControlCenterConfiguration const* self ); +gint foobar_control_center_configuration_get_offset ( FoobarControlCenterConfiguration const* self ); +gint foobar_control_center_configuration_get_padding ( FoobarControlCenterConfiguration const* self ); +gint foobar_control_center_configuration_get_spacing ( FoobarControlCenterConfiguration const* self ); +FoobarOrientation foobar_control_center_configuration_get_orientation( FoobarControlCenterConfiguration const* self ); +FoobarControlCenterAlignment foobar_control_center_configuration_get_alignment ( FoobarControlCenterConfiguration const* self ); +FoobarControlCenterRow const* foobar_control_center_configuration_get_rows ( FoobarControlCenterConfiguration const* self, + gsize* out_count ); +void foobar_control_center_configuration_set_width ( FoobarControlCenterConfiguration* self, + gint value ); +void foobar_control_center_configuration_set_height ( FoobarControlCenterConfiguration* self, + gint value ); +void foobar_control_center_configuration_set_position ( FoobarControlCenterConfiguration* self, + FoobarScreenEdge value ); +void foobar_control_center_configuration_set_offset ( FoobarControlCenterConfiguration* self, + gint value ); +void foobar_control_center_configuration_set_padding ( FoobarControlCenterConfiguration* self, + gint value ); +void foobar_control_center_configuration_set_spacing ( FoobarControlCenterConfiguration* self, + gint value ); +void foobar_control_center_configuration_set_orientation( FoobarControlCenterConfiguration* self, + FoobarOrientation value ); +void foobar_control_center_configuration_set_rows ( FoobarControlCenterConfiguration* self, + FoobarControlCenterRow const* value, + gsize value_count ); +void foobar_control_center_configuration_set_alignment ( FoobarControlCenterConfiguration* self, + FoobarControlCenterAlignment value ); + +typedef struct _FoobarNotificationConfiguration FoobarNotificationConfiguration; + +GType foobar_notification_configuration_get_type ( void ); +FoobarNotificationConfiguration* foobar_notification_configuration_new ( void ); +FoobarNotificationConfiguration* foobar_notification_configuration_copy ( FoobarNotificationConfiguration const* self ); +void foobar_notification_configuration_free ( FoobarNotificationConfiguration* self ); +gboolean foobar_notification_configuration_equal ( FoobarNotificationConfiguration const* a, + FoobarNotificationConfiguration const* b ); +gint foobar_notification_configuration_get_width ( FoobarNotificationConfiguration const* self ); +gint foobar_notification_configuration_get_min_height ( FoobarNotificationConfiguration const* self ); +gint foobar_notification_configuration_get_spacing ( FoobarNotificationConfiguration const* self ); +gint foobar_notification_configuration_get_close_button_inset( FoobarNotificationConfiguration const* self ); +gchar const* foobar_notification_configuration_get_time_format ( FoobarNotificationConfiguration const* self ); +void foobar_notification_configuration_set_width ( FoobarNotificationConfiguration* self, + gint value ); +void foobar_notification_configuration_set_min_height ( FoobarNotificationConfiguration* self, + gint value ); +void foobar_notification_configuration_set_spacing ( FoobarNotificationConfiguration* self, + gint value ); +void foobar_notification_configuration_set_close_button_inset( FoobarNotificationConfiguration* self, + gint value ); +void foobar_notification_configuration_set_time_format ( FoobarNotificationConfiguration* self, + gchar const* value ); + +typedef struct _FoobarConfiguration FoobarConfiguration; + +GType foobar_configuration_get_type ( void ); +FoobarConfiguration* foobar_configuration_new ( void ); +FoobarConfiguration* foobar_configuration_copy ( FoobarConfiguration const* self ); +void foobar_configuration_free ( FoobarConfiguration* self ); +gboolean foobar_configuration_equal ( FoobarConfiguration const* a, + FoobarConfiguration const* b ); +FoobarGeneralConfiguration const* foobar_configuration_get_general ( FoobarConfiguration const* self ); +FoobarGeneralConfiguration* foobar_configuration_get_general_mut ( FoobarConfiguration* self ); +FoobarPanelConfiguration const* foobar_configuration_get_panel ( FoobarConfiguration const* self ); +FoobarPanelConfiguration* foobar_configuration_get_panel_mut ( FoobarConfiguration* self ); +FoobarLauncherConfiguration const* foobar_configuration_get_launcher ( FoobarConfiguration const* self ); +FoobarLauncherConfiguration* foobar_configuration_get_launcher_mut ( FoobarConfiguration* self ); +FoobarControlCenterConfiguration const* foobar_configuration_get_control_center ( FoobarConfiguration const* self ); +FoobarControlCenterConfiguration* foobar_configuration_get_control_center_mut( FoobarConfiguration* self ); +FoobarNotificationConfiguration const* foobar_configuration_get_notifications ( FoobarConfiguration const* self ); +FoobarNotificationConfiguration* foobar_configuration_get_notifications_mut ( FoobarConfiguration* self ); + + +G_DECLARE_FINAL_TYPE( FoobarConfigurationService, foobar_configuration_service, FOOBAR, CONFIGURATION_SERVICE, GObject ) + +FoobarConfigurationService* foobar_configuration_service_new ( void ); +FoobarConfiguration const* foobar_configuration_service_get_current( FoobarConfigurationService* self ); + +G_END_DECLS diff --git a/csrc/services/meson.build b/csrc/services/meson.build new file mode 100644 index 0000000..368c2f1 --- /dev/null +++ b/csrc/services/meson.build @@ -0,0 +1,15 @@ +foobar_sources += files( + 'battery-service.c', + 'clock-service.c', + 'brightness-service.c', + 'workspace-service.c', + 'notification-service.c', + 'audio-service.c', + 'network-service.c', + 'bluetooth-service.c', + 'application-service.c', + 'quick-answer-service.c', + 'configuration-service.c', +) + +subdir('quick-answers') diff --git a/csrc/services/network-service.c b/csrc/services/network-service.c new file mode 100644 index 0000000..b2d1334 --- /dev/null +++ b/csrc/services/network-service.c @@ -0,0 +1,1577 @@ +#include "services/network-service.h" +#include +#include +#include + +typedef struct _ApNetworkMembership ApNetworkMembership; + +#define SCAN_REFRESH_INTERVAL 10000 + +// +// FoobarNetwork: +// +// Represents a Wi-Fi network, identified by its SSID. Instances contain a set of NMAccessPoints and the strength is +// updated to match that of the best available AP. +// + +struct _FoobarNetwork +{ + GObject parent_instance; + GBytes* ssid; + gchar* name; + gint strength; + gboolean is_active; + GHashTable* aps; +}; + +enum +{ + NETWORK_PROP_NAME = 1, + NETWORK_PROP_STRENGTH, + NETWORK_PROP_IS_ACTIVE, + N_NETWORK_PROPS, +}; + +static GParamSpec* network_props[N_NETWORK_PROPS] = { 0 }; + +static void foobar_network_class_init ( FoobarNetworkClass* klass ); +static void foobar_network_init ( FoobarNetwork* self ); +static void foobar_network_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_network_finalize ( GObject* object ); +static FoobarNetwork* foobar_network_new ( void ); +static void foobar_network_set_ssid ( FoobarNetwork* self, + GBytes* value ); +static void foobar_network_set_active ( FoobarNetwork* self, + gboolean value ); +static void foobar_network_add_ap ( FoobarNetwork* self, + NMAccessPoint* ap ); +static void foobar_network_remove_ap ( FoobarNetwork* self, + NMAccessPoint* ap ); +static void foobar_network_update_strength ( FoobarNetwork* self ); +static void foobar_network_handle_strength_change( GObject* ap, + GParamSpec* pspec, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarNetwork, foobar_network, G_TYPE_OBJECT ) + +// +// FoobarNetworkAdapterState: +// +// The current connection/connectivity state of a network adapter. +// + +G_DEFINE_ENUM_TYPE( + FoobarNetworkAdapterState, + foobar_network_adapter_state, + G_DEFINE_ENUM_VALUE( FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED, "disconnected" ), + G_DEFINE_ENUM_VALUE( FOOBAR_NETWORK_ADAPTER_STATE_CONNECTING, "connecting" ), + G_DEFINE_ENUM_VALUE( FOOBAR_NETWORK_ADAPTER_STATE_CONNECTED, "connected" ), + G_DEFINE_ENUM_VALUE( FOOBAR_NETWORK_ADAPTER_STATE_LIMITED, "limited" ) ) + +// +// FoobarNetworkAdapter: +// +// Base class for Wi-Fi/ethernet network adapters. +// + +typedef struct _FoobarNetworkAdapterPrivate +{ + NMDevice* device; + NMActiveConnection* connection; + FoobarNetworkAdapterState state; + gulong connectivity_handler_id; + gulong connection_handler_id; + gulong connection_state_handler_id; +} FoobarNetworkAdapterPrivate; + +enum +{ + ADAPTER_PROP_STATE = 1, + N_ADAPTER_PROPS, +}; + +static GParamSpec* adapter_props[N_ADAPTER_PROPS] = { 0 }; + +static void foobar_network_adapter_class_init ( FoobarNetworkAdapterClass* klass ); +static void foobar_network_adapter_init ( FoobarNetworkAdapter* self ); +static void foobar_network_adapter_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_network_adapter_finalize ( GObject* object ); +static void foobar_network_adapter_set_device ( FoobarNetworkAdapter* self, + NMDevice* value ); +static void foobar_network_adapter_update_state ( FoobarNetworkAdapter* self ); +static void foobar_network_adapter_handle_state_change ( GObject* sender, + GParamSpec* pspec, + gpointer userdata ); +static void foobar_network_adapter_handle_connection_change( GObject* sender, + GParamSpec* pspec, + gpointer userdata ); + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE( FoobarNetworkAdapter, foobar_network_adapter, G_TYPE_OBJECT ) + +// +// FoobarNetworkAdapterWired: +// +// Represents an ethernet network adapter. +// + +struct _FoobarNetworkAdapterWired +{ + FoobarNetworkAdapter parent_instance; + NMDeviceEthernet* device; + gulong speed_handler_id; +}; + +static void foobar_network_adapter_wired_class_init ( FoobarNetworkAdapterWiredClass* klass ); +static void foobar_network_adapter_wired_init ( FoobarNetworkAdapterWired* self ); +static void foobar_network_adapter_wired_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_network_adapter_wired_finalize ( GObject* object ); +static FoobarNetworkAdapterWired* foobar_network_adapter_wired_new ( NMDeviceEthernet* device ); +static void foobar_network_adapter_wired_handle_speed_change( GObject* device, + GParamSpec* pspec, + gpointer userdata ); + +enum +{ + WIRED_PROP_SPEED = 1, + N_WIRED_PROPS, +}; + +static GParamSpec* wired_props[N_WIRED_PROPS] = { 0 }; + +G_DEFINE_FINAL_TYPE( FoobarNetworkAdapterWired, foobar_network_adapter_wired, FOOBAR_TYPE_NETWORK_ADAPTER ) + +// +// FoobarNetworkAdapterWifi: +// +// Represents a Wi-Fi network adapter. +// + +struct _FoobarNetworkAdapterWifi +{ + FoobarNetworkAdapter parent_instance; + GHashTable* ap_networks; + GHashTable* named_networks; + GListStore* networks; + GtkFilterListModel* filtered_networks; + GtkSortListModel* sorted_networks; + FoobarNetwork* active; + gboolean is_scanning; + NMClient* client; + NMDeviceWifi* device; + gulong enabled_handler_id; + gulong active_handler_id; + gulong added_handler_id; + gulong removed_handler_id; + guint refresh_source_id; +}; + +enum +{ + WIFI_PROP_NETWORKS = 1, + WIFI_PROP_ACTIVE, + WIFI_PROP_IS_ENABLED, + WIFI_PROP_IS_SCANNING, + N_WIFI_PROPS, +}; + +static GParamSpec* wifi_props[N_WIFI_PROPS] = { 0 }; + +static void foobar_network_adapter_wifi_class_init ( FoobarNetworkAdapterWifiClass* klass ); +static void foobar_network_adapter_wifi_init ( FoobarNetworkAdapterWifi* self ); +static void foobar_network_adapter_wifi_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_network_adapter_wifi_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_network_adapter_wifi_finalize ( GObject* object ); +static FoobarNetworkAdapterWifi* foobar_network_adapter_wifi_new ( NMClient* client, + NMDeviceWifi* device ); +static void foobar_network_adapter_wifi_add_ap_to_network ( FoobarNetworkAdapterWifi* self, + NMAccessPoint* ap ); +static void foobar_network_adapter_wifi_update_active ( FoobarNetworkAdapterWifi* self ); +static void foobar_network_adapter_wifi_handle_ssid_change ( GObject* ap, + GParamSpec* pspec, + gpointer userdata ); +static void foobar_network_adapter_wifi_handle_enabled_change( GObject* client, + GParamSpec* pspec, + gpointer userdata ); +static void foobar_network_adapter_wifi_handle_active_change ( GObject* device, + GParamSpec* pspec, + gpointer userdata ); +static void foobar_network_adapter_wifi_handle_added ( NMDeviceWifi* device, + NMAccessPoint* ap, + gpointer userdata ); +static void foobar_network_adapter_wifi_handle_removed ( NMDeviceWifi* device, + NMAccessPoint* ap, + gpointer userdata ); +static gboolean foobar_network_adapter_wifi_refresh_list ( gpointer userdata ); +static gboolean foobar_network_adapter_wifi_filter_func ( gpointer item, + gpointer userdata ); +static gint foobar_network_adapter_wifi_sort_func ( gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarNetworkAdapterWifi, foobar_network_adapter_wifi, FOOBAR_TYPE_NETWORK_ADAPTER ) + +// +// ApNetworkMembership: +// +// Structure representing an AP's membership in a network. The AP is removed when the structure is destroyed. +// + +struct _ApNetworkMembership +{ + NMAccessPoint* ap; + FoobarNetwork* network; +}; + +static ApNetworkMembership* ap_network_membership_new ( NMAccessPoint* ap, + FoobarNetwork* network ); +static void ap_network_membership_destroy( ApNetworkMembership* self ); + +// +// FoobarNetworkService: +// +// Service monitoring the network state of the computer. This is implemented using the NetworkManager client library. +// + +struct _FoobarNetworkService +{ + GObject parent_instance; + NMClient* client; + FoobarNetworkAdapterWired* wired; + FoobarNetworkAdapterWifi* wifi; + FoobarNetworkAdapter* active; + gulong primary_handler_id; + gulong activating_handler_id; +}; + +enum +{ + PROP_WIRED = 1, + PROP_WIFI, + PROP_ACTIVE, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_network_service_class_init ( FoobarNetworkServiceClass* klass ); +static void foobar_network_service_init ( FoobarNetworkService* self ); +static void foobar_network_service_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_network_service_finalize ( GObject* object ); +static void foobar_network_service_handle_connection_change( GObject* client, + GParamSpec* pspec, + gpointer userdata ); +static void foobar_network_service_update_active ( FoobarNetworkService* self ); + +G_DEFINE_FINAL_TYPE( FoobarNetworkService, foobar_network_service, G_TYPE_OBJECT ) + +// --------------------------------------------------------------------------------------------------------------------- +// Wi-Fi Network +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for networks. +// +void foobar_network_class_init( FoobarNetworkClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_network_get_property; + object_klass->finalize = foobar_network_finalize; + + network_props[NETWORK_PROP_NAME] = g_param_spec_string( + "name", + "Name", + "The advertised SSID of the network.", + NULL, + G_PARAM_READABLE ); + network_props[NETWORK_PROP_STRENGTH] = g_param_spec_int( + "strength", + "Strength", + "The strength of the network in percent.", + 0, + 100, + 0, + G_PARAM_READABLE ); + network_props[NETWORK_PROP_IS_ACTIVE] = g_param_spec_boolean( + "is-active", + "Is Active", + "Indicates whether the network is currently used by the client.", + FALSE, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_NETWORK_PROPS, network_props ); +} + +// +// Instance initialization for networks. +// +void foobar_network_init( FoobarNetwork* self ) +{ + self->aps = g_hash_table_new_full( g_direct_hash, g_direct_equal, NULL, NULL ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_network_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarNetwork* self = (FoobarNetwork*)object; + + switch ( prop_id ) + { + case NETWORK_PROP_NAME: + g_value_set_string( value, foobar_network_get_name( self ) ); + break; + case NETWORK_PROP_STRENGTH: + g_value_set_int( value, foobar_network_get_strength( self ) ); + break; + case NETWORK_PROP_IS_ACTIVE: + g_value_set_boolean( value, foobar_network_is_active( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for networks. +// +void foobar_network_finalize( GObject* object ) +{ + FoobarNetwork* self = (FoobarNetwork*)object; + + GHashTableIter iter; + gpointer ap; + g_hash_table_iter_init( &iter, self->aps ); + while ( g_hash_table_iter_next( &iter, &ap, NULL ) ) + { + g_signal_handlers_disconnect_by_data( ap, self ); + } + + g_clear_pointer( &self->ssid, g_bytes_unref ); + g_clear_pointer( &self->name, g_free ); + g_clear_pointer( &self->aps, g_hash_table_unref ); + + G_OBJECT_CLASS( foobar_network_parent_class )->finalize( object ); +} + +// +// Create a new network instance. +// +FoobarNetwork* foobar_network_new( void ) +{ + return g_object_new( FOOBAR_TYPE_NETWORK, NULL ); +} + +// +// Get the advertized SSID of the network. +// +gchar const* foobar_network_get_name( FoobarNetwork* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NETWORK( self ), NULL ); + return self->name; +} + +// +// Get the current strength percentage value for the network. +// +gint foobar_network_get_strength( FoobarNetwork* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NETWORK( self ), 0 ); + return self->strength; +} + +// +// Check whether the network is currently used by the client. +// +gboolean foobar_network_is_active( FoobarNetwork* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NETWORK( self ), FALSE ); + return self->is_active; +} + +// +// Update the advertized SSID of the network. +// +// This also updates the string representation (which may be a lossy conversion). +// +void foobar_network_set_ssid( + FoobarNetwork* self, + GBytes* value ) +{ + g_return_if_fail( FOOBAR_IS_NETWORK( self ) ); + + if ( self->ssid != value ) + { + g_clear_pointer( &self->ssid, g_bytes_unref ); + g_clear_pointer( &self->name, g_free ); + + if ( value ) + { + self->ssid = g_bytes_ref( value ); + gsize ssid_len; + guint8 const* ssid_data = g_bytes_get_data( self->ssid, &ssid_len ); + self->name = nm_utils_ssid_to_utf8( ssid_data, ssid_len ); + } + + g_object_notify_by_pspec( G_OBJECT( self ), network_props[NETWORK_PROP_NAME] ); + } +} + +// +// Update the "active" flag of the network. This does not actually activate the network. +// +void foobar_network_set_active( + FoobarNetwork* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_NETWORK( self ) ); + + value = !!value; + if ( self->is_active != value ) + { + self->is_active = value; + g_object_notify_by_pspec( G_OBJECT( self ), network_props[NETWORK_PROP_NAME] ); + } +} + +// +// Include the specified AP when computing the network's strength. +// +void foobar_network_add_ap( + FoobarNetwork* self, + NMAccessPoint* ap ) +{ + if ( g_hash_table_add( self->aps, g_object_ref( ap ) ) ) + { + g_signal_connect( ap, "notify::strength", G_CALLBACK( foobar_network_handle_strength_change ), self ); + foobar_network_update_strength( self ); + } +} + +// +// Stop including the specified AP when computing the network's strength. +// +void foobar_network_remove_ap( + FoobarNetwork* self, + NMAccessPoint* ap ) +{ + if ( g_hash_table_remove( self->aps, ap ) ) + { + g_signal_handlers_disconnect_by_data( ap, self ); + foobar_network_update_strength( self ); + } +} + +// +// Update the network's "strength" property to match that of the best AP for it. +// +void foobar_network_update_strength( FoobarNetwork* self ) +{ + gint strength = 0; + GHashTableIter iter; + gpointer ap; + g_hash_table_iter_init( &iter, self->aps ); + while ( g_hash_table_iter_next( &iter, &ap, NULL ) ) + { + gint ap_strength = nm_access_point_get_strength( ap ); + strength = MAX( strength, ap_strength ); + } + + strength = CLAMP( strength, 0, 100 ); + if ( self->strength != strength ) + { + self->strength = strength; + g_object_notify_by_pspec( G_OBJECT( self ), network_props[NETWORK_PROP_STRENGTH] ); + } +} + +// +// Called when the strength of an access point for this network has changed. +// +void foobar_network_handle_strength_change( + GObject* ap, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)ap; + (void)pspec; + FoobarNetwork* self = (FoobarNetwork*)userdata; + + foobar_network_update_strength( self ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Network Adapter +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for network adapters. +// +void foobar_network_adapter_class_init( FoobarNetworkAdapterClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_network_adapter_get_property; + object_klass->finalize = foobar_network_adapter_finalize; + + adapter_props[ADAPTER_PROP_STATE] = g_param_spec_enum( + "state", + "State", + "The current connection state of the network.", + FOOBAR_TYPE_NETWORK_ADAPTER_STATE, + FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_ADAPTER_PROPS, adapter_props ); +} + +// +// Instance initialization for network adapters. +// +void foobar_network_adapter_init( FoobarNetworkAdapter* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_network_adapter_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarNetworkAdapter* self = (FoobarNetworkAdapter*)object; + + switch ( prop_id ) + { + case ADAPTER_PROP_STATE: + g_value_set_enum( value, foobar_network_adapter_get_state( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for network adapters. +// +void foobar_network_adapter_finalize( GObject* object ) +{ + FoobarNetworkAdapter* self = (FoobarNetworkAdapter*)object; + + foobar_network_adapter_set_device( self, NULL ); + + G_OBJECT_CLASS( foobar_network_adapter_parent_class )->finalize( object ); +} + +// +// Get the current connection/connectivity state of the adapter. +// +FoobarNetworkAdapterState foobar_network_adapter_get_state( FoobarNetworkAdapter* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER( self ), FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED ); + + FoobarNetworkAdapterPrivate* priv = foobar_network_adapter_get_instance_private( self ); + return priv->state; +} + +// +// Update the backing network device which is used to derive the adapter's state. +// +void foobar_network_adapter_set_device( + FoobarNetworkAdapter* self, + NMDevice* value ) +{ + FoobarNetworkAdapterPrivate* priv = foobar_network_adapter_get_instance_private( self ); + if ( priv->device != value ) + { + g_clear_signal_handler( &priv->connection_state_handler_id, priv->connection ); + g_clear_signal_handler( &priv->connectivity_handler_id, priv->device ); + g_clear_signal_handler( &priv->connection_handler_id, priv->device ); + g_clear_object( &priv->connection ); + g_clear_object( &priv->device ); + + priv->device = value; + if ( priv->device ) + { + g_object_ref( priv->device ); + priv->connectivity_handler_id = g_signal_connect( + priv->device, + "notify::ip4-connectivity", + G_CALLBACK( foobar_network_adapter_handle_state_change ), self ); + priv->connection_handler_id = g_signal_connect( + priv->device, + "notify::active-connection", + G_CALLBACK( foobar_network_adapter_handle_connection_change ), self ); + priv->connection = nm_device_get_active_connection( priv->device ); + if ( priv->connection ) + { + g_object_ref( priv->connection ); + priv->connection_state_handler_id = g_signal_connect( + priv->connection, + "notify::state", + G_CALLBACK( foobar_network_adapter_handle_state_change ), + self ); + } + } + + foobar_network_adapter_update_state( self ); + } +} + +// +// Derive the connection/connectivity state for the network from the backing device. +// +void foobar_network_adapter_update_state( FoobarNetworkAdapter* self ) +{ + FoobarNetworkAdapterPrivate* priv = foobar_network_adapter_get_instance_private( self ); + + FoobarNetworkAdapterState state = FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED; + if ( priv->device ) + { + NMActiveConnection* connection = nm_device_get_active_connection( priv->device ); + if ( connection ) + { + switch ( nm_active_connection_get_state( connection ) ) + { + case NM_ACTIVE_CONNECTION_STATE_ACTIVATED: + state = ( nm_device_get_connectivity( priv->device, AF_INET ) == NM_CONNECTIVITY_FULL ) + ? FOOBAR_NETWORK_ADAPTER_STATE_CONNECTED + : FOOBAR_NETWORK_ADAPTER_STATE_LIMITED; + break; + case NM_ACTIVE_CONNECTION_STATE_ACTIVATING: + state = FOOBAR_NETWORK_ADAPTER_STATE_CONNECTING; + break; + case NM_ACTIVE_CONNECTION_STATE_DEACTIVATING: + case NM_ACTIVE_CONNECTION_STATE_DEACTIVATED: + case NM_ACTIVE_CONNECTION_STATE_UNKNOWN: + default: + state = FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED; + break; + } + } + } + + if ( priv->state != state ) + { + priv->state = state; + g_object_notify_by_pspec( G_OBJECT( self ), adapter_props[ADAPTER_PROP_STATE] ); + } +} + +// +// Called when the "ip4-connectivity" property of the NetworkManager device or the "state" property of the active +// connection has changed. +// +static void foobar_network_adapter_handle_state_change( + GObject* sender, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)sender; + (void)pspec; + FoobarNetworkAdapter* self = (FoobarNetworkAdapter*)userdata; + + foobar_network_adapter_update_state( self ); +} + +// +// Called when the "active-connection" property of the NetworkManager device has changed. +// +// This updates the state of the adapter and sets the connection up for state change listening. +// +static void foobar_network_adapter_handle_connection_change( + GObject* sender, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)sender; + (void)pspec; + FoobarNetworkAdapter* self = (FoobarNetworkAdapter*)userdata; + FoobarNetworkAdapterPrivate* priv = foobar_network_adapter_get_instance_private( self ); + + g_clear_signal_handler( &priv->connection_state_handler_id, priv->connection ); + g_clear_object( &priv->connection ); + + priv->connection = nm_device_get_active_connection( priv->device ); + if ( priv->connection ) + { + g_object_ref( priv->connection ); + priv->connection_state_handler_id = g_signal_connect( + priv->connection, + "notify::state", + G_CALLBACK( foobar_network_adapter_handle_state_change ), + self ); + } + + foobar_network_adapter_update_state( self ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Wired Network Adapter +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for wired network adapters. +// +void foobar_network_adapter_wired_class_init( FoobarNetworkAdapterWiredClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_network_adapter_wired_get_property; + object_klass->finalize = foobar_network_adapter_wired_finalize; + + wired_props[WIRED_PROP_SPEED] = g_param_spec_int64( + "speed", + "Speed", + "The supported speed of the adapter in Mbit/s.", + 0, + INT64_MAX, + 0, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_WIRED_PROPS, wired_props ); +} + +// +// Instance initialization for wired network adapters. +// +void foobar_network_adapter_wired_init( FoobarNetworkAdapterWired* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_network_adapter_wired_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarNetworkAdapterWired* self = (FoobarNetworkAdapterWired*)object; + + switch ( prop_id ) + { + case WIRED_PROP_SPEED: + g_value_set_int64( value, foobar_network_adapter_wired_get_speed( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for wired network adapters. +// +void foobar_network_adapter_wired_finalize( GObject* object ) +{ + FoobarNetworkAdapterWired* self = (FoobarNetworkAdapterWired*)object; + + g_clear_signal_handler( &self->speed_handler_id, self->device ); + g_clear_object( &self->device ); + + G_OBJECT_CLASS( foobar_network_adapter_wired_parent_class )->finalize( object ); +} + +// +// Create a new wired network adapter wrapping the provided NetworkManager device. +// +FoobarNetworkAdapterWired* foobar_network_adapter_wired_new( NMDeviceEthernet* device ) +{ + FoobarNetworkAdapterWired* self = g_object_new( FOOBAR_TYPE_NETWORK_ADAPTER_WIRED, NULL ); + foobar_network_adapter_set_device( FOOBAR_NETWORK_ADAPTER( self ), NM_DEVICE( device ) ); + + self->device = g_object_ref( device ); + self->speed_handler_id = g_signal_connect( + self->device, + "notify::speed", + G_CALLBACK( foobar_network_adapter_wired_handle_speed_change ), + self ); + return self; +} + +// +// Get the supported speed of the adapter in Mbit/s. +// +gint64 foobar_network_adapter_wired_get_speed( FoobarNetworkAdapterWired* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIRED( self ), 0 ); + return nm_device_ethernet_get_speed( self->device ); +} + +// +// Called when the "speed" property of the NetworkManager device changes. +// +void foobar_network_adapter_wired_handle_speed_change( + GObject* device, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)device; + (void)pspec; + FoobarNetworkAdapterWired* self = (FoobarNetworkAdapterWired*)userdata; + + g_object_notify_by_pspec( G_OBJECT( self ), wired_props[WIRED_PROP_SPEED] ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Wireless Network Adapter +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for wireless network adapters. +// +void foobar_network_adapter_wifi_class_init( FoobarNetworkAdapterWifiClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_network_adapter_wifi_get_property; + object_klass->set_property = foobar_network_adapter_wifi_set_property; + object_klass->finalize = foobar_network_adapter_wifi_finalize; + + wifi_props[WIFI_PROP_NETWORKS] = g_param_spec_object( + "networks", + "Networks", + "Sorted list of available networks.", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE ); + wifi_props[WIFI_PROP_ACTIVE] = g_param_spec_object( + "active", + "Active", + "The currently active network (if any).", + FOOBAR_TYPE_NETWORK, + G_PARAM_READABLE ); + wifi_props[WIFI_PROP_IS_ENABLED] = g_param_spec_boolean( + "is-enabled", + "Is Enabled", + "Indicates whether the wifi adapter is currently enabled.", + FALSE, + G_PARAM_READWRITE ); + wifi_props[WIFI_PROP_IS_SCANNING] = g_param_spec_boolean( + "is-scanning", + "Is Scanning", + "Indicates whether the wifi adapter is currently scanning for available networks.", + FALSE, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_WIFI_PROPS, wifi_props ); +} + +// +// Instance initialization for wireless network adapters. +// +void foobar_network_adapter_wifi_init( FoobarNetworkAdapterWifi* self ) +{ + self->ap_networks = g_hash_table_new_full( + g_direct_hash, + g_direct_equal, + g_object_unref, + (GDestroyNotify)ap_network_membership_destroy ); + self->named_networks = g_hash_table_new_full( + g_bytes_hash, + g_bytes_equal, + (GDestroyNotify)g_bytes_unref, + g_object_unref ); + self->networks = g_list_store_new( FOOBAR_TYPE_NETWORK ); + + GtkCustomFilter* filter = gtk_custom_filter_new( foobar_network_adapter_wifi_filter_func, NULL, NULL ); + self->filtered_networks = gtk_filter_list_model_new( + G_LIST_MODEL( g_object_ref( self->networks ) ), + GTK_FILTER( filter ) ); + + GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_network_adapter_wifi_sort_func, NULL, NULL ); + self->sorted_networks = gtk_sort_list_model_new( + G_LIST_MODEL( g_object_ref( self->filtered_networks ) ), + GTK_SORTER( sorter ) ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_network_adapter_wifi_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)object; + + switch ( prop_id ) + { + case WIFI_PROP_NETWORKS: + g_value_set_object( value, foobar_network_adapter_wifi_get_networks( self ) ); + break; + case WIFI_PROP_ACTIVE: + g_value_set_object( value, foobar_network_adapter_wifi_get_active( self ) ); + break; + case WIFI_PROP_IS_ENABLED: + g_value_set_boolean( value, foobar_network_adapter_wifi_is_enabled( self ) ); + break; + case WIFI_PROP_IS_SCANNING: + g_value_set_boolean( value, foobar_network_adapter_wifi_is_scanning( 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_network_adapter_wifi_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)object; + + switch ( prop_id ) + { + case WIFI_PROP_IS_ENABLED: + foobar_network_adapter_wifi_set_enabled( self, g_value_get_boolean( value ) ); + break; + case WIFI_PROP_IS_SCANNING: + foobar_network_adapter_wifi_set_scanning( self, g_value_get_boolean( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for wireless network adapters. +// +void foobar_network_adapter_wifi_finalize( GObject* object ) +{ + FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)object; + + GPtrArray const* aps = nm_device_wifi_get_access_points( self->device ); + for ( guint i = 0; i < aps->len; ++i ) + { + NMAccessPoint* ap = g_ptr_array_index( aps, i ); + g_signal_handlers_disconnect_by_data( ap, self ); + } + + g_clear_handle_id( &self->refresh_source_id, g_source_remove ); + g_clear_signal_handler( &self->enabled_handler_id, self->client ); + g_clear_signal_handler( &self->active_handler_id, self->device ); + g_clear_signal_handler( &self->added_handler_id, self->device ); + g_clear_signal_handler( &self->removed_handler_id, self->device ); + g_clear_object( &self->client ); + g_clear_object( &self->device ); + g_clear_object( &self->sorted_networks ); + g_clear_object( &self->filtered_networks ); + g_clear_object( &self->networks ); + g_clear_object( &self->active ); + g_clear_pointer( &self->named_networks, g_hash_table_unref ); + g_clear_pointer( &self->ap_networks, g_hash_table_unref ); + + G_OBJECT_CLASS( foobar_network_adapter_wifi_parent_class )->finalize( object ); +} + +// +// Create a new wireless network adapter wrapping the provided NetworkManager device. +// +FoobarNetworkAdapterWifi* foobar_network_adapter_wifi_new( + NMClient* client, + NMDeviceWifi* device ) +{ + FoobarNetworkAdapterWifi* self = g_object_new( FOOBAR_TYPE_NETWORK_ADAPTER_WIFI, NULL ); + foobar_network_adapter_set_device( FOOBAR_NETWORK_ADAPTER( self ), NM_DEVICE( device ) ); + + self->client = g_object_ref( client ); + self->device = g_object_ref( device ); + self->enabled_handler_id = g_signal_connect( + self->client, + "notify::wireless-enabled", + G_CALLBACK( foobar_network_adapter_wifi_handle_enabled_change ), + self ); + self->active_handler_id = g_signal_connect( + self->device, + "notify::active-access-point", + G_CALLBACK( foobar_network_adapter_wifi_handle_active_change ), + self ); + self->added_handler_id = g_signal_connect( + self->device, + "access-point-added", + G_CALLBACK( foobar_network_adapter_wifi_handle_added ), + self ); + self->removed_handler_id = g_signal_connect( + self->device, + "access-point-removed", + G_CALLBACK( foobar_network_adapter_wifi_handle_removed ), + self ); + + GPtrArray const* aps = nm_device_wifi_get_access_points( self->device ); + for ( guint i = 0; i < aps->len; ++i ) + { + NMAccessPoint* ap = g_ptr_array_index( aps, i ); + g_signal_connect( + ap, + "notify::ssid", + G_CALLBACK( foobar_network_adapter_wifi_handle_ssid_change ), + self ); + foobar_network_adapter_wifi_add_ap_to_network( self, ap ); + } + + foobar_network_adapter_wifi_update_active( self ); + return self; +} + +// +// Get a sorted list of available networks. +// +GListModel* foobar_network_adapter_wifi_get_networks( FoobarNetworkAdapterWifi* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ), NULL ); + return G_LIST_MODEL( self->sorted_networks ); +} + +// +// Get the currently active network (if any). +// +FoobarNetwork* foobar_network_adapter_wifi_get_active( FoobarNetworkAdapterWifi* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ), NULL ); + return self->active; +} + +// +// Check whether the wifi adapter is currently enabled. +// +gboolean foobar_network_adapter_wifi_is_enabled( FoobarNetworkAdapterWifi* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ), FALSE ); + return nm_client_wireless_get_enabled( self->client ); +} + +// +// Check whether the wifi adapter is currently scanning for available networks. +// +gboolean foobar_network_adapter_wifi_is_scanning( FoobarNetworkAdapterWifi* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ), FALSE ); + return self->is_scanning; +} + +// +// Update whether the wifi adapter is currently enabled. +// +void foobar_network_adapter_wifi_set_enabled( + FoobarNetworkAdapterWifi* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ) ); + + if ( !value ) { foobar_network_adapter_wifi_set_scanning( self, FALSE ); } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + nm_client_wireless_set_enabled( self->client, value ); +#pragma GCC diagnostic pop +} + +// +// Update whether the wifi adapter is currently scanning for available networks. +// +void foobar_network_adapter_wifi_set_scanning( + FoobarNetworkAdapterWifi* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ) ); + + value = value && foobar_network_adapter_wifi_is_enabled( self ); + if ( self->is_scanning != value ) + { + self->is_scanning = value; + g_object_notify_by_pspec( G_OBJECT( self ), wifi_props[WIFI_PROP_IS_SCANNING] ); + + // Periodically start scans until "is-scanning" is disabled again. + + g_clear_handle_id( &self->refresh_source_id, g_source_remove ); + if ( self->is_scanning ) + { + foobar_network_adapter_wifi_refresh_list( self ); + self->refresh_source_id = g_timeout_add( + SCAN_REFRESH_INTERVAL, + foobar_network_adapter_wifi_refresh_list, + self ); + } + } +} + +// +// Ensure that a network for the specified access point exists. +// +// If the AP has an SSID, it is added to the network for this SSID. Otherwise, a new network for this AP is created just +// for this network. +// +void foobar_network_adapter_wifi_add_ap_to_network( + FoobarNetworkAdapterWifi* self, + NMAccessPoint* ap ) +{ + // The AP is automatically added/removed from the network when using g_hash_table_replace because of the + // ApNetworkMembership destroy function. + + GBytes* ssid = nm_access_point_get_ssid( ap ); + if ( ssid ) + { + FoobarNetwork* network = g_hash_table_lookup( self->named_networks, ssid ); + if ( !network ) + { + network = foobar_network_new( ); + foobar_network_set_ssid( network, ssid ); + g_list_store_append( self->networks, network ); + g_hash_table_insert( self->named_networks, g_bytes_ref( ssid ), network ); + } + + g_hash_table_replace( self->ap_networks, g_object_ref( ap ), ap_network_membership_new( ap, network ) ); + } + else + { + g_autoptr( FoobarNetwork ) network = foobar_network_new( ); + g_hash_table_replace( self->ap_networks, g_object_ref( ap ), ap_network_membership_new( ap, network ) ); + } + + gtk_filter_changed( gtk_filter_list_model_get_filter( self->filtered_networks ), GTK_FILTER_CHANGE_DIFFERENT ); +} + +// +// Update the active network based on the active access point. +// +void foobar_network_adapter_wifi_update_active( FoobarNetworkAdapterWifi* self ) +{ + NMAccessPoint* active_ap = nm_device_wifi_get_active_access_point( self->device ); + ApNetworkMembership* membership = active_ap ? g_hash_table_lookup( self->ap_networks, active_ap ) : NULL; + FoobarNetwork* new_active = membership ? membership->network : NULL; + + if ( self->active != new_active ) + { + if ( self->active ) + { + foobar_network_set_active( self->active, FALSE ); + } + g_clear_object( &self->active ); + + self->active = new_active; + if ( self->active ) + { + g_object_ref( self->active ); + foobar_network_set_active( self->active, TRUE ); + } + + g_object_notify_by_pspec( G_OBJECT( self ), wifi_props[WIFI_PROP_ACTIVE] ); + } +} + +// +// Called when the SSID for an access point changes. +// +// This will remove the access point from the old network and associate it with its new network. +// +void foobar_network_adapter_wifi_handle_ssid_change( + GObject* ap, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)pspec; + FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata; + + foobar_network_adapter_wifi_add_ap_to_network( self, NM_ACCESS_POINT( ap ) ); + foobar_network_adapter_wifi_update_active( self ); +} + +// +// Called when the "wireless-enabled" state of the client changes. +// +void foobar_network_adapter_wifi_handle_enabled_change( + GObject* client, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)client; + (void)pspec; + FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata; + + g_object_notify_by_pspec( G_OBJECT( self ), wifi_props[WIFI_PROP_IS_ENABLED] ); +} + +// +// Called when the active access point of the backing device changes. +// +void foobar_network_adapter_wifi_handle_active_change( + GObject* device, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)device; + (void)pspec; + FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata; + + foobar_network_adapter_wifi_update_active( self ); +} + +// +// Called when a new access point is added. +// +// This will associate the access point with its network. +// +void foobar_network_adapter_wifi_handle_added( + NMDeviceWifi* device, + NMAccessPoint* ap, + gpointer userdata ) +{ + (void)device; + FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata; + + g_signal_connect( + ap, + "notify::ssid", + G_CALLBACK( foobar_network_adapter_wifi_handle_ssid_change ), + self ); + foobar_network_adapter_wifi_add_ap_to_network( self, ap ); +} + +// +// Called when an access point is removed. +// +// This will remove it from its network. +// +void foobar_network_adapter_wifi_handle_removed( + NMDeviceWifi* device, + NMAccessPoint* ap, + gpointer userdata ) +{ + (void)device; + FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata; + + g_signal_handlers_disconnect_by_data( ap, self ); + g_hash_table_remove( self->ap_networks, ap ); // Will remove the AP from the network. + gtk_filter_changed( gtk_filter_list_model_get_filter( self->filtered_networks ), GTK_FILTER_CHANGE_MORE_STRICT ); +} + +// +// Periodic callback to start a network scan. +// +gboolean foobar_network_adapter_wifi_refresh_list( gpointer userdata ) +{ + FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata; + + g_autoptr( GError ) error = NULL; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + if ( !nm_device_wifi_request_scan( self->device, NULL, &error ) ) +#pragma GCC diagnostic pop + { + g_warning( "Unable to start Wi-Fi network scan: %s", error->message ); + } + + return G_SOURCE_CONTINUE; +} + +// +// Filtering callback for the list of networks. Only networks with at least one AP are shown. +// +gboolean foobar_network_adapter_wifi_filter_func( + gpointer item, + gpointer userdata ) +{ + (void)userdata; + + FoobarNetwork* network = (FoobarNetwork*)item; + return g_hash_table_size( network->aps ) > 0; +} + +// +// Sorting callback for the list of networks. +// +gint foobar_network_adapter_wifi_sort_func( + gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ) +{ + (void)userdata; + + FoobarNetwork* network_a = (FoobarNetwork*)item_a; + FoobarNetwork* network_b = (FoobarNetwork*)item_b; + return g_strcmp0( foobar_network_get_name( network_a ), foobar_network_get_name( network_b ) ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Access Point Network Memberships +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create an association between an access point and the network it belongs to. +// +ApNetworkMembership* ap_network_membership_new( + NMAccessPoint* ap, + FoobarNetwork* network ) +{ + ApNetworkMembership* self = g_new0( ApNetworkMembership, 1 ); + self->ap = ap; + self->network = g_object_ref( network ); + foobar_network_add_ap( self->network, self->ap ); + return self; +} + +// +// Destroy an association between an access point and its previous network. +// +void ap_network_membership_destroy( ApNetworkMembership* self ) +{ + if ( self ) + { + foobar_network_remove_ap( self->network, self->ap ); + g_object_unref( self->network ); + g_free( self ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the network service. +// +void foobar_network_service_class_init( FoobarNetworkServiceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_network_service_get_property; + object_klass->finalize = foobar_network_service_finalize; + + props[PROP_WIRED] = g_param_spec_object( + "wired", + "Wired", + "The wired network adapter (if available).", + FOOBAR_TYPE_NETWORK_ADAPTER_WIRED, + G_PARAM_READABLE ); + props[PROP_WIFI] = g_param_spec_object( + "wifi", + "WiFi", + "The wireless network adapter (if available).", + FOOBAR_TYPE_NETWORK_ADAPTER_WIFI, + G_PARAM_READABLE ); + props[PROP_ACTIVE] = g_param_spec_object( + "active", + "Active", + "The network adapter currently in use.", + FOOBAR_TYPE_NETWORK_ADAPTER, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for the network service. +// +void foobar_network_service_init( FoobarNetworkService* self ) +{ + g_autoptr( GError ) error = NULL; + self->client = nm_client_new( NULL, &error ); + if ( !self->client ) + { + g_warning( "Unable to connect to NetworkManager: %s", error->message ); + return; + } + + NMDeviceEthernet* device_wired = NULL; + NMDeviceWifi* device_wifi = NULL; + GPtrArray const* devices = nm_client_get_devices( self->client ); + for ( guint i = 0; i < devices->len; ++i ) + { + // XXX: Add support for multiple devices + NMDevice* device = g_ptr_array_index( devices, i ); + if ( !device_wired && NM_IS_DEVICE_ETHERNET( device ) ) + { + device_wired = NM_DEVICE_ETHERNET( device ); + } + if ( !device_wifi && NM_IS_DEVICE_WIFI( device ) ) + { + device_wifi = NM_DEVICE_WIFI( device ); + } + if ( device_wired && device_wifi ) + { + break; + } + } + + self->wired = device_wired ? foobar_network_adapter_wired_new( device_wired ) : NULL; + self->wifi = device_wifi ? foobar_network_adapter_wifi_new( self->client, device_wifi ) : NULL; + + foobar_network_service_update_active( self ); + self->primary_handler_id = g_signal_connect( + self->client, + "notify::primary-connection", + G_CALLBACK( foobar_network_service_handle_connection_change ), + self ); + self->activating_handler_id = g_signal_connect( + self->client, + "notify::activating-connection", + G_CALLBACK( foobar_network_service_handle_connection_change ), + self ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_network_service_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarNetworkService* self = (FoobarNetworkService*)object; + + switch ( prop_id ) + { + case PROP_WIRED: + g_value_set_object( value, foobar_network_service_get_wired( self ) ); + break; + case PROP_WIFI: + g_value_set_object( value, foobar_network_service_get_wifi( self ) ); + break; + case PROP_ACTIVE: + g_value_set_object( value, foobar_network_service_get_active( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for the network service. +// +void foobar_network_service_finalize( GObject* object ) +{ + FoobarNetworkService* self = (FoobarNetworkService*)object; + + g_clear_signal_handler( &self->primary_handler_id, self->client ); + g_clear_signal_handler( &self->activating_handler_id, self->client ); + g_clear_object( &self->wifi ); + g_clear_object( &self->wired ); + g_clear_object( &self->client ); + + G_OBJECT_CLASS( foobar_network_service_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new network service instance. +// +FoobarNetworkService* foobar_network_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_NETWORK_SERVICE, NULL ); +} + +// +// Get the wired network adapter (if available). +// +FoobarNetworkAdapterWired* foobar_network_service_get_wired( FoobarNetworkService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NETWORK_SERVICE( self ), NULL ); + return self->wired; +} + +// +// Get the wireless network adapter (if available). +// +FoobarNetworkAdapterWifi* foobar_network_service_get_wifi( FoobarNetworkService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NETWORK_SERVICE( self ), NULL ); + return self->wifi; +} + +// +// Get the network adapter currently in use. +// +FoobarNetworkAdapter* foobar_network_service_get_active( FoobarNetworkService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NETWORK_SERVICE( self ), NULL ); + return self->active; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called when either the primary connection or the activating connection of the client has changed. +// +void foobar_network_service_handle_connection_change( + GObject* client, + GParamSpec* pspec, + gpointer userdata ) +{ + (void)client; + (void)pspec; + FoobarNetworkService* self = (FoobarNetworkService*)userdata; + + foobar_network_service_update_active( self ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Update the active network adapter. +// +// This uses the primary connection of the client to determine whether the wireless or wired adapter is used. Otherwise, +// the activating connection is used instead. +// +void foobar_network_service_update_active( FoobarNetworkService* self ) +{ + NMActiveConnection* connection = nm_client_get_primary_connection( self->client ); + connection = connection ? connection : nm_client_get_activating_connection( self->client ); + gchar const* type = connection ? nm_active_connection_get_connection_type( connection ) : NULL; + + FoobarNetworkAdapter* new_active; + if ( !g_strcmp0( type, "802-11-wireless" ) ) + { + new_active = FOOBAR_NETWORK_ADAPTER( self->wifi ); + } + else if ( !g_strcmp0( type, "802-3-ethernet" ) ) + { + new_active = FOOBAR_NETWORK_ADAPTER( self->wired ); + } + else + { + new_active = self->wifi ? FOOBAR_NETWORK_ADAPTER( self->wifi ) : FOOBAR_NETWORK_ADAPTER( self->wired ); + } + + if ( self->active != new_active ) + { + self->active = new_active; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ACTIVE] ); + } +} diff --git a/csrc/services/network-service.h b/csrc/services/network-service.h new file mode 100644 index 0000000..2307cec --- /dev/null +++ b/csrc/services/network-service.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_NETWORK foobar_network_get_type( ) +#define FOOBAR_TYPE_NETWORK_ADAPTER_STATE foobar_network_adapter_state_get_type( ) +#define FOOBAR_TYPE_NETWORK_ADAPTER foobar_network_adapter_get_type( ) +#define FOOBAR_TYPE_NETWORK_ADAPTER_WIRED foobar_network_adapter_wired_get_type( ) +#define FOOBAR_TYPE_NETWORK_ADAPTER_WIFI foobar_network_adapter_wifi_get_type( ) +#define FOOBAR_TYPE_NETWORK_SERVICE foobar_network_service_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarNetwork, foobar_network, FOOBAR, NETWORK, GObject ) + +gchar const* foobar_network_get_name ( FoobarNetwork* self ); +gint foobar_network_get_strength( FoobarNetwork* self ); +gboolean foobar_network_is_active ( FoobarNetwork* self ); + +typedef enum +{ + FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED, + FOOBAR_NETWORK_ADAPTER_STATE_CONNECTING, + FOOBAR_NETWORK_ADAPTER_STATE_CONNECTED, + FOOBAR_NETWORK_ADAPTER_STATE_LIMITED, +} FoobarNetworkAdapterState; + +GType foobar_network_adapter_state_get_type( void ); + +G_DECLARE_DERIVABLE_TYPE( FoobarNetworkAdapter, foobar_network_adapter, FOOBAR, NETWORK_ADAPTER, GObject ) + +struct _FoobarNetworkAdapterClass +{ + GObjectClass parent_class; +}; + +FoobarNetworkAdapterState foobar_network_adapter_get_state( FoobarNetworkAdapter* self ); + +G_DECLARE_FINAL_TYPE( FoobarNetworkAdapterWired, foobar_network_adapter_wired, FOOBAR, NETWORK_ADAPTER_WIRED, FoobarNetworkAdapter ) + +gint64 foobar_network_adapter_wired_get_speed( FoobarNetworkAdapterWired* self ); + +G_DECLARE_FINAL_TYPE( FoobarNetworkAdapterWifi, foobar_network_adapter_wifi, FOOBAR, NETWORK_ADAPTER_WIFI, FoobarNetworkAdapter ) + +GListModel* foobar_network_adapter_wifi_get_networks( FoobarNetworkAdapterWifi* self ); +FoobarNetwork* foobar_network_adapter_wifi_get_active ( FoobarNetworkAdapterWifi* self ); +gboolean foobar_network_adapter_wifi_is_enabled ( FoobarNetworkAdapterWifi* self ); +gboolean foobar_network_adapter_wifi_is_scanning ( FoobarNetworkAdapterWifi* self ); +void foobar_network_adapter_wifi_set_enabled ( FoobarNetworkAdapterWifi* self, + gboolean value ); +void foobar_network_adapter_wifi_set_scanning( FoobarNetworkAdapterWifi* self, + gboolean value ); + +G_DECLARE_FINAL_TYPE( FoobarNetworkService, foobar_network_service, FOOBAR, NETWORK_SERVICE, GObject ) + +FoobarNetworkService* foobar_network_service_new ( void ); +FoobarNetworkAdapterWired* foobar_network_service_get_wired ( FoobarNetworkService* self ); +FoobarNetworkAdapterWifi* foobar_network_service_get_wifi ( FoobarNetworkService* self ); +FoobarNetworkAdapter* foobar_network_service_get_active( FoobarNetworkService* self ); + +G_END_DECLS diff --git a/csrc/services/notification-service.c b/csrc/services/notification-service.c new file mode 100644 index 0000000..1a085f0 --- /dev/null +++ b/csrc/services/notification-service.c @@ -0,0 +1,1833 @@ +#include "services/notification-service.h" +#include "dbus/notifications.h" +#include "utils.h" +#include +#include + +#define DEFAULT_TIMEOUT 3000 + +// +// FoobarNotificationUrgency: +// +// The urgency of a notification as provided by the application that sent it. +// + +G_DEFINE_ENUM_TYPE( + FoobarNotificationUrgency, + foobar_notification_urgency, + G_DEFINE_ENUM_VALUE( FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_COMFORT, "loss-of-comfort" ), + G_DEFINE_ENUM_VALUE( FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY, "loss-of-money" ), + G_DEFINE_ENUM_VALUE( FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_LIFE, "loss-of-life" ) ) + +// +// FoobarNotificationAction: +// +// A labeled action for a notification that can be invoked by the notification daemon (us). +// + +struct _FoobarNotificationAction +{ + GObject parent_instance; + FoobarNotification* notification; + gchar* id; + gchar* label; +}; + +enum +{ + ACTION_PROP_ID = 1, + ACTION_PROP_LABEL, + N_ACTION_PROPS, +}; + +static GParamSpec* action_props[N_ACTION_PROPS] = { 0 }; + +static void foobar_notification_action_class_init ( FoobarNotificationActionClass* klass ); +static void foobar_notification_action_init ( FoobarNotificationAction* self ); +static void foobar_notification_action_get_property( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_notification_action_finalize ( GObject* object ); +static FoobarNotificationAction* foobar_notification_action_new ( FoobarNotification* notification ); +static void foobar_notification_action_set_id ( FoobarNotificationAction* self, + gchar const* value ); +static void foobar_notification_action_set_label ( FoobarNotificationAction* self, + gchar const* value ); + +G_DEFINE_FINAL_TYPE( FoobarNotificationAction, foobar_notification_action, G_TYPE_OBJECT ) + +// +// FoobarNotification: +// +// A notification received by the notification daemon (us). +// + +struct _FoobarNotification +{ + GObject parent_instance; + guint timeout_id; + FoobarNotificationService* service; + guint id; + GPtrArray* actions; + gchar* app_entry; + gchar* app_name; + gchar* body; + gchar* summary; + GdkPixbuf* image; + gchar* image_path; + gchar* image_data; + gboolean is_dismissed; + gboolean is_resident; + gboolean is_transient; + GDateTime* time; + gint64 timeout; + FoobarNotificationUrgency urgency; +}; + +enum +{ + NOTIFICATION_PROP_ID = 1, + NOTIFICATION_PROP_APP_ENTRY, + NOTIFICATION_PROP_APP_NAME, + NOTIFICATION_PROP_BODY, + NOTIFICATION_PROP_SUMMARY, + NOTIFICATION_PROP_IMAGE, + NOTIFICATION_PROP_IS_DISMISSED, + NOTIFICATION_PROP_IS_RESIDENT, + NOTIFICATION_PROP_IS_TRANSIENT, + NOTIFICATION_PROP_TIME, + NOTIFICATION_PROP_TIMEOUT, + NOTIFICATION_PROP_URGENCY, + N_NOTIFICATION_PROPS, +}; + +static GParamSpec* notification_props[N_NOTIFICATION_PROPS] = { 0 }; + +static void foobar_notification_class_init ( FoobarNotificationClass* klass ); +static void foobar_notification_init ( FoobarNotification* self ); +static void foobar_notification_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_notification_finalize ( GObject* object ); +static FoobarNotification* foobar_notification_new ( FoobarNotificationService* service ); +static gchar const* foobar_notification_get_image_path ( FoobarNotification* self ); +static gchar const* foobar_notification_get_image_data ( FoobarNotification* self ); +static void foobar_notification_set_id ( FoobarNotification* self, + guint value ); +static void foobar_notification_set_app_entry ( FoobarNotification* self, + gchar const* value ); +static void foobar_notification_set_app_name ( FoobarNotification* self, + gchar const* value ); +static void foobar_notification_set_body ( FoobarNotification* self, + gchar const* value ); +static void foobar_notification_set_summary ( FoobarNotification* self, + gchar const* value ); +static void foobar_notification_set_image_from_path( FoobarNotification* self, + gchar const* value ); +static void foobar_notification_set_image_from_data( FoobarNotification* self, + gchar const* value ); +static void foobar_notification_set_image ( FoobarNotification* self, + GdkPixbuf* value ); +static void foobar_notification_set_dismissed ( FoobarNotification* self, + gboolean value ); +static void foobar_notification_set_resident ( FoobarNotification* self, + gboolean value ); +static void foobar_notification_set_transient ( FoobarNotification* self, + gboolean value ); +static void foobar_notification_set_time ( FoobarNotification* self, + GDateTime* value ); +static void foobar_notification_set_timeout ( FoobarNotification* self, + gint64 value ); +static void foobar_notification_set_urgency ( FoobarNotification* self, + FoobarNotificationUrgency value ); +static void foobar_notification_add_action ( FoobarNotification* self, + FoobarNotificationAction* action ); +static void foobar_notification_free_action ( gpointer action ); +static gboolean foobar_notification_handle_timeout ( gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarNotification, foobar_notification, G_TYPE_OBJECT ) + +// +// FoobarNotificationService: +// +// Service acting as the notification daemon, receiving incoming notifications from other applications. The service also +// persistently saves previously received notifications until they are closed. +// + +struct _FoobarNotificationService +{ + GObject parent_instance; + GListStore* notifications; + GtkSortListModel* sorted_notifications; + GtkFilterListModel* popup_notifications; + FoobarNotifications* skeleton; + guint bus_owner_id; + guint next_id; + gchar* cache_path; + GMutex write_cache_mutex; +}; + +enum +{ + PROP_NOTIFICATIONS = 1, + PROP_POPUP_NOTIFICATIONS, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_notification_service_class_init ( FoobarNotificationServiceClass* klass ); +static void foobar_notification_service_init ( FoobarNotificationService* self ); +static void foobar_notification_service_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_notification_service_finalize ( GObject* object ); +static void foobar_notification_service_read_cache ( FoobarNotificationService* self ); +static void foobar_notification_service_write_cache ( FoobarNotificationService* self ); +static void foobar_notification_service_write_cache_cb ( GObject* object, + GAsyncResult* result, + gpointer userdata ); +static void foobar_notification_service_write_cache_async ( FoobarNotificationService* self, + GPtrArray* notifications, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer userdata ); +static gboolean foobar_notification_service_write_cache_finish ( FoobarNotificationService* self, + GAsyncResult* result, + GError** error ); +static void foobar_notification_service_write_cache_thread ( GTask* task, + gpointer source_object, + gpointer task_data, + GCancellable* cancellable ); +static void foobar_notification_service_handle_bus_acquired ( GDBusConnection* connection, + gchar const* name, + gpointer userdata ); +static void foobar_notification_service_handle_bus_lost ( GDBusConnection* connection, + gchar const* name, + gpointer userdata ); +static gboolean foobar_notification_service_handle_notify ( FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + gchar const* app_name, + guint replaces_id, + gchar const* image, + gchar const* summary, + gchar const* body, + gchar const* const* actions, + GVariant* hints, + gint expiration, + gpointer userdata ); +static gboolean foobar_notification_service_handle_close_notification ( FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + guint id, + gpointer userdata ); +static gboolean foobar_notification_service_handle_get_capabilities ( FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + gpointer userdata ); +static gboolean foobar_notification_service_handle_get_server_information( FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + gpointer userdata ); +static gboolean foobar_notification_service_popup_filter_func ( gpointer item, + gpointer userdata ); +static gint foobar_notification_service_sort_func ( gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarNotificationService, foobar_notification_service, G_TYPE_OBJECT ) + +// --------------------------------------------------------------------------------------------------------------------- +// Notification Action +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for notification actions. +// +void foobar_notification_action_class_init( FoobarNotificationActionClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_notification_action_get_property; + object_klass->finalize = foobar_notification_action_finalize; + + action_props[ACTION_PROP_ID] = g_param_spec_string( + "id", + "ID", + "Textual ID of the action within the notification.", + NULL, + G_PARAM_READABLE ); + action_props[ACTION_PROP_LABEL] = g_param_spec_string( + "label", + "Label", + "Human-readable title for the action.", + NULL, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_ACTION_PROPS, action_props ); +} + +// +// Instance initialization for notification actions. +// +void foobar_notification_action_init( FoobarNotificationAction* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_notification_action_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarNotificationAction* self = (FoobarNotificationAction*)object; + + switch ( prop_id ) + { + case ACTION_PROP_ID: + g_value_set_string( value, foobar_notification_action_get_id( self ) ); + break; + case ACTION_PROP_LABEL: + g_value_set_string( value, foobar_notification_action_get_label( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for notification actions. +// +void foobar_notification_action_finalize( GObject* object ) +{ + FoobarNotificationAction* self = (FoobarNotificationAction*)object; + + g_clear_pointer( &self->id, g_free ); + g_clear_pointer( &self->label, g_free ); + + G_OBJECT_CLASS( foobar_notification_action_parent_class )->finalize( object ); +} + +// +// Create a new notification action that is owned by the given notification (captured as an unowned reference). +// +FoobarNotificationAction* foobar_notification_action_new( FoobarNotification* notification ) +{ + FoobarNotificationAction* self = g_object_new( FOOBAR_TYPE_NOTIFICATION_ACTION, NULL ); + self->notification = notification; + return self; +} + +// +// Get the textual ID of the action within the notification. +// +gchar const* foobar_notification_action_get_id( FoobarNotificationAction* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ), NULL ); + return self->id; +} + +// +// Get a human-readable title for the action. +// +gchar const* foobar_notification_action_get_label( FoobarNotificationAction* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ), NULL ); + return self->label; +} + +// +// Update the textual ID of the action within the notification. +// +void foobar_notification_action_set_id( + FoobarNotificationAction* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ) ); + + if ( g_strcmp0( self->id, value ) ) + { + g_clear_pointer( &self->id, g_free ); + self->id = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), action_props[ACTION_PROP_ID] ); + } +} + +// +// Set a human-readable title for the action. +// +void foobar_notification_action_set_label( + FoobarNotificationAction* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ) ); + + if ( g_strcmp0( self->label, value ) ) + { + g_clear_pointer( &self->label, g_free ); + self->label = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), action_props[ACTION_PROP_LABEL] ); + } +} + +// +// Invoke this action by emitting a signal that can be handled by its sender application. +// +void foobar_notification_action_invoke( FoobarNotificationAction* self ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ) ); + + if ( self->notification && self->notification->service ) + { + FoobarNotification* notification = self->notification; + FoobarNotificationService* service = notification->service; + + foobar_notifications_emit_action_invoked( + service->skeleton, + foobar_notification_get_id( notification ), + foobar_notification_action_get_id( self ) ); + + if ( !foobar_notification_is_resident( notification ) ) + { + foobar_notification_close( notification ); + } + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Notification +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for notifications. +// +void foobar_notification_class_init( FoobarNotificationClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_notification_get_property; + object_klass->finalize = foobar_notification_finalize; + + notification_props[NOTIFICATION_PROP_ID] = g_param_spec_uint( + "id", + "ID", + "Numeric ID of the notification.", + 0, + UINT_MAX, + 0, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_APP_ENTRY] = g_param_spec_string( + "app-entry", + "App Entry", + "The desktop file of the notification's application.", + NULL, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_APP_NAME] = g_param_spec_string( + "app-name", + "App Name", + "Human-readable name of the application.", + NULL, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_BODY] = g_param_spec_string( + "body", + "Body", + "Main content of the notification.", + NULL, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_SUMMARY] = g_param_spec_string( + "summary", + "Summary", + "Brief summary of the notification's content.", + NULL, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_IMAGE] = g_param_spec_object( + "image", + "Image", + "An image icon for the notification.", + GDK_TYPE_PIXBUF, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_IS_DISMISSED] = g_param_spec_boolean( + "is-dismissed", + "Is Dismissed", + "Indicates that the notification was dismissed or the timeout has passed.", + FALSE, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_IS_RESIDENT] = g_param_spec_boolean( + "is-resident", + "Is Resident", + "Indicates that the notification is not automatically removed once an action is invoked.", + FALSE, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_IS_TRANSIENT] = g_param_spec_boolean( + "is-transient", + "Is Transient", + "Indicates that the persistence capability should be bypassed.", + FALSE, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_TIME] = g_param_spec_boxed( + "time", + "Time", + "The timestamp at which the notification was received.", + G_TYPE_DATE_TIME, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_TIMEOUT] = g_param_spec_int64( + "timeout", + "Timeout", + "Timeout after which the notification auto-closes in milliseconds.", + 0, + INT64_MAX, + 0, + G_PARAM_READABLE ); + notification_props[NOTIFICATION_PROP_URGENCY] = g_param_spec_enum( + "urgency", + "Urgency", + "The urgency state of the notification.", + FOOBAR_TYPE_NOTIFICATION_URGENCY, + FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_NOTIFICATION_PROPS, notification_props ); +} + +// +// Instance initialization for notifications. +// +void foobar_notification_init( FoobarNotification* self ) +{ + self->actions = g_ptr_array_new_with_free_func( foobar_notification_free_action ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_notification_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarNotification* self = (FoobarNotification*)object; + + switch ( prop_id ) + { + case NOTIFICATION_PROP_ID: + g_value_set_int64( value, foobar_notification_get_id( self ) ); + break; + case NOTIFICATION_PROP_APP_ENTRY: + g_value_set_string( value, foobar_notification_get_app_entry( self ) ); + break; + case NOTIFICATION_PROP_APP_NAME: + g_value_set_string( value, foobar_notification_get_app_name( self ) ); + break; + case NOTIFICATION_PROP_BODY: + g_value_set_string( value, foobar_notification_get_body( self ) ); + break; + case NOTIFICATION_PROP_SUMMARY: + g_value_set_string( value, foobar_notification_get_summary( self ) ); + break; + case NOTIFICATION_PROP_IMAGE: + g_value_set_object( value, foobar_notification_get_image( self ) ); + break; + case NOTIFICATION_PROP_IS_DISMISSED: + g_value_set_boolean( value, foobar_notification_is_dismissed( self ) ); + break; + case NOTIFICATION_PROP_IS_RESIDENT: + g_value_set_boolean( value, foobar_notification_is_resident( self ) ); + break; + case NOTIFICATION_PROP_IS_TRANSIENT: + g_value_set_boolean( value, foobar_notification_is_transient( self ) ); + break; + case NOTIFICATION_PROP_TIME: + g_value_set_boxed( value, foobar_notification_get_time( self ) ); + break; + case NOTIFICATION_PROP_TIMEOUT: + g_value_set_int64( value, foobar_notification_get_timeout( self ) ); + break; + case NOTIFICATION_PROP_URGENCY: + g_value_set_enum( value, foobar_notification_get_urgency( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance initialization for notifications. +// +void foobar_notification_finalize( GObject* object ) +{ + FoobarNotification* self = (FoobarNotification*)object; + + g_clear_object( &self->image ); + g_clear_pointer( &self->actions, g_ptr_array_unref ); + g_clear_pointer( &self->app_entry, g_free ); + g_clear_pointer( &self->app_name, g_free ); + g_clear_pointer( &self->body, g_free ); + g_clear_pointer( &self->summary, g_free ); + g_clear_pointer( &self->image_path, g_free ); + g_clear_pointer( &self->image_data, g_free ); + g_clear_pointer( &self->time, g_date_time_unref ); + + G_OBJECT_CLASS( foobar_notification_parent_class )->finalize( object ); +} + +// +// Create a new notification that is owned by the given service (captured as an unowned reference). +// +FoobarNotification* foobar_notification_new( FoobarNotificationService* service ) +{ + FoobarNotification* self = g_object_new( FOOBAR_TYPE_NOTIFICATION, NULL ); + self->service = service; + return self; +} + +// +// Numeric ID of the notification. +// +guint foobar_notification_get_id( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), 0 ); + return self->id; +} + +// +// A list of actions which can be invoked by notifying the notification's sender. +// +FoobarNotificationAction** foobar_notification_get_actions( + FoobarNotification* self, + guint* out_count ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + if ( out_count ) { *out_count = self->actions->len; } + return (FoobarNotificationAction**)self->actions->pdata; +} + +// +// The desktop file of the notification's application. +// +gchar const* foobar_notification_get_app_entry( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->app_entry; +} + +// +// Human-readable name of the application. +// +gchar const* foobar_notification_get_app_name( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->app_name; +} + +// +// Main content of the notification. +// +gchar const* foobar_notification_get_body( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->body; +} + +// +// Brief summary of the notification's content. +// +gchar const* foobar_notification_get_summary( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->summary; +} + +// +// The source path for the notification's image if it was loaded from a file. +// +gchar const* foobar_notification_get_image_path( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->image_path; +} + +// +// The base64-encoded data for the notification's image if it was loaded from data. +// +gchar const* foobar_notification_get_image_data( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->image_data; +} + +// +// An image icon for the notification. +// +GdkPixbuf* foobar_notification_get_image( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->image; +} + +// +// Indicates that the notification was dismissed or the timeout has passed. +// +gboolean foobar_notification_is_dismissed( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FALSE ); + return self->is_dismissed; +} + +// +// Indicates that the notification is not automatically removed once an action is invoked. +// +gboolean foobar_notification_is_resident( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FALSE ); + return self->is_resident; +} + +// +// Indicates that the persistence capability should be bypassed. +// +gboolean foobar_notification_is_transient( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FALSE ); + return self->is_transient; +} + +// +// The timestamp at which the notification was received. +// +GDateTime* foobar_notification_get_time( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); + return self->time; +} + +// +// Timeout after which the notification auto-closes in milliseconds. +// +gint64 foobar_notification_get_timeout( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), 0 ); + return self->timeout; +} + +// +// The urgency state of the notification. +// +FoobarNotificationUrgency foobar_notification_get_urgency( FoobarNotification* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY ); + return self->urgency; +} + +// +// Numeric ID of the notification. +// +void foobar_notification_set_id( + FoobarNotification* self, + guint value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( self->id != value ) + { + self->id = value; + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_ID] ); + } +} + +// +// The desktop file of the notification's application. +// +void foobar_notification_set_app_entry( + FoobarNotification* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( g_strcmp0( self->app_entry, value ) ) + { + g_clear_pointer( &self->app_entry, g_free ); + self->app_entry = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_APP_ENTRY] ); + } +} + +// +// Human-readable name of the application. +// +void foobar_notification_set_app_name( + FoobarNotification* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( g_strcmp0( self->app_name, value ) ) + { + g_clear_pointer( &self->app_name, g_free ); + self->app_name = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_APP_NAME] ); + } +} + +// +// Main content of the notification. +// +void foobar_notification_set_body( + FoobarNotification* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( g_strcmp0( self->body, value ) ) + { + g_clear_pointer( &self->body, g_free ); + self->body = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_BODY] ); + } +} + +// +// Brief summary of the notification's content. +// +void foobar_notification_set_summary( + FoobarNotification* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( g_strcmp0( self->summary, value ) ) + { + g_clear_pointer( &self->summary, g_free ); + self->summary = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_SUMMARY] ); + } +} + +// +// Update the notification's image by trying to load a file at the provided path. +// +void foobar_notification_set_image_from_path( + FoobarNotification* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( g_strcmp0( self->image_path, value ) ) + { + g_clear_pointer( &self->image_path, g_free ); + g_clear_pointer( &self->image_data, g_free ); + g_clear_object( &self->image ); + self->image_path = g_strdup( value ); + + g_autoptr( GError ) error = NULL; + self->image = gdk_pixbuf_new_from_file( self->image_path, &error ); + if ( !self->image ) + { + g_warning( "Invalid image path for notification: %s", error->message ); + } + + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IMAGE] ); + } +} + +// +// Update the notification's image by trying to load base64-encoded data. +// +void foobar_notification_set_image_from_data( + FoobarNotification* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( g_strcmp0( self->image_data, value ) ) + { + g_clear_pointer( &self->image_path, g_free ); + g_clear_pointer( &self->image_data, g_free ); + g_clear_object( &self->image ); + self->image_data = g_strdup( value ); + + gsize data_len; + guchar* data = g_base64_decode( self->image_data, &data_len ); + g_autoptr( GError ) error = NULL; + g_autoptr( GInputStream ) input = g_memory_input_stream_new_from_data( data, (gssize)data_len, g_free ); + self->image = gdk_pixbuf_new_from_stream( input, NULL, &error ); + if ( !self->image ) + { + g_warning( "Invalid image data for notification: %s", error->message ); + } + + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IMAGE] ); + } +} + +// +// Update the notification's image to a custom pixbuf. +// +static void foobar_notification_set_image( + FoobarNotification* self, + GdkPixbuf* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( self->image != value ) + { + g_clear_pointer( &self->image_path, g_free ); + g_clear_pointer( &self->image_data, g_free ); + g_clear_object( &self->image ); + + if ( value ) + { + self->image = g_object_ref( value ); + g_autoptr( GError ) error = NULL; + gchar* data; + gsize data_len; + if ( gdk_pixbuf_save_to_buffer( self->image, &data, &data_len, "png", &error, NULL ) ) + { + self->image_data = g_base64_encode( (guchar const*)data, data_len ); + } + else + { + g_warning( "Unable to save image for notification: %s", error->message ); + g_clear_object( &self->image ); + } + } + + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IMAGE] ); + } +} + +// +// Indicates that the notification was dismissed or the timeout has passed. +// +void foobar_notification_set_dismissed( + FoobarNotification* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + value = !!value; + if ( self->is_dismissed != value ) + { + self->is_dismissed = value; + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IS_DISMISSED] ); + + if ( self->service ) + { + gtk_filter_changed( + gtk_filter_list_model_get_filter( self->service->popup_notifications ), + value ? GTK_FILTER_CHANGE_MORE_STRICT : GTK_FILTER_CHANGE_LESS_STRICT ); + } + } +} + +// +// Indicates that the notification is not automatically removed once an action is invoked. +// +void foobar_notification_set_resident( + FoobarNotification* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + value = !!value; + if ( self->is_resident != value ) + { + self->is_resident = value; + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IS_RESIDENT] ); + } +} + +// +// Indicates that the persistence capability should be bypassed. +// +void foobar_notification_set_transient( + FoobarNotification* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + value = !!value; + if ( self->is_transient != value ) + { + self->is_transient = value; + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IS_TRANSIENT] ); + } +} + +// +// The timestamp at which the notification was received. +// +void foobar_notification_set_time( + FoobarNotification* self, + GDateTime* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( self->time != value ) + { + if ( self->time ) { g_date_time_unref( self->time ); } + self->time = g_date_time_ref( value ); + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_TIME] ); + } +} + +// +// Timeout after which the notification auto-closes in milliseconds. +// +void foobar_notification_set_timeout( + FoobarNotification* self, + gint64 value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + value = MAX( value, 0 ); + if ( self->timeout != value ) + { + self->timeout = value; + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_TIMEOUT] ); + } +} + +// +// The urgency state of the notification. +// +void foobar_notification_set_urgency( + FoobarNotification* self, + FoobarNotificationUrgency value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( self->urgency != value ) + { + self->urgency = value; + g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_URGENCY] ); + } +} + +// +// Register an action for the notification. +// +void foobar_notification_add_action( + FoobarNotification* self, + FoobarNotificationAction* action ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + g_return_if_fail( action->notification == self ); + + g_ptr_array_add( self->actions, g_object_ref( action ) ); +} + +// +// Block the notification from automatically being dismissed. +// +void foobar_notification_block_timeout( FoobarNotification* self ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + g_clear_handle_id( &self->timeout_id, g_source_remove ); +} + +// +// If not already started, start the timeout to automatically dismiss the notification after it's timeout. +// +void foobar_notification_resume_timeout( FoobarNotification* self ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( !self->is_dismissed && !self->timeout_id ) + { + self->timeout_id = g_timeout_add_full( + G_NOTIFICATION_PRIORITY_NORMAL, + self->timeout, + foobar_notification_handle_timeout, + g_object_ref( self ), + g_object_unref ); + } +} + +// +// Manually dismiss the notification, setting "is-dismissed" to TRUE. +// +void foobar_notification_dismiss( FoobarNotification* self ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + foobar_notification_set_dismissed( self, TRUE ); +} + +// +// Close the notification, removing it from the parent service's list. +// +void foobar_notification_close( FoobarNotification* self ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); + + if ( self->service ) + { + FoobarNotificationService* service = self->service; + self->service = NULL; + + guint id = foobar_notification_get_id( self ); + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( service->notifications ) ); ++i ) + { + if ( g_list_model_get_item( G_LIST_MODEL( service->notifications ), i ) == self ) + { + g_list_store_remove( service->notifications, i ); + foobar_notification_service_write_cache( service ); + break; + } + } + + foobar_notifications_emit_notification_closed( service->skeleton, id, 3 ); + } +} + +// +// Called when an action is removed from a notification, resetting its weak reference to the notification. +// +void foobar_notification_free_action( gpointer action ) +{ + FoobarNotificationAction* object = (FoobarNotificationAction*)action; + object->notification = NULL; + g_object_unref( object ); +} + +// +// Called after the notification's timeout has elapsed. +// +gboolean foobar_notification_handle_timeout( gpointer userdata ) +{ + FoobarNotification* notification = (FoobarNotification*)userdata; + + foobar_notification_dismiss( notification ); + notification->timeout_id = 0; + + return G_SOURCE_REMOVE; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the notification service. +// +void foobar_notification_service_class_init( FoobarNotificationServiceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_notification_service_get_property; + object_klass->finalize = foobar_notification_service_finalize; + + props[PROP_NOTIFICATIONS] = g_param_spec_object( + "notifications", + "Notifications", + "Sorted list of all notifications.", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE ); + props[PROP_POPUP_NOTIFICATIONS] = g_param_spec_object( + "popup-notifications", + "Popup Notifications", + "Sorted list of all visible notifications (i.e. notifications that are not dismissed).", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for the notification service. +// +void foobar_notification_service_init( FoobarNotificationService* self ) +{ + self->notifications = g_list_store_new( FOOBAR_TYPE_NOTIFICATION ); + self->cache_path = foobar_get_cache_path( "notifications.json" ); + g_mutex_init( &self->write_cache_mutex ); + + GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_notification_service_sort_func, NULL, NULL ); + self->sorted_notifications = gtk_sort_list_model_new( + G_LIST_MODEL( g_object_ref( self->notifications ) ), + GTK_SORTER( sorter ) ); + + GtkCustomFilter* popup_filter = gtk_custom_filter_new( foobar_notification_service_popup_filter_func, NULL, NULL ); + self->popup_notifications = gtk_filter_list_model_new( + G_LIST_MODEL( g_object_ref( self->sorted_notifications ) ), + GTK_FILTER( popup_filter ) ); + + foobar_notification_service_read_cache( self ); + + self->bus_owner_id = g_bus_own_name( + G_BUS_TYPE_SESSION, + "org.freedesktop.Notifications", + G_BUS_NAME_OWNER_FLAGS_NONE, + foobar_notification_service_handle_bus_acquired, + NULL, + foobar_notification_service_handle_bus_lost, + self, + NULL ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_notification_service_get_property( GObject* object, guint prop_id, GValue* value, GParamSpec* pspec ) +{ + FoobarNotificationService* self = (FoobarNotificationService*)object; + + switch ( prop_id ) + { + case PROP_NOTIFICATIONS: + g_value_set_object( value, foobar_notification_service_get_notifications( self ) ); + break; + case PROP_POPUP_NOTIFICATIONS: + g_value_set_object( value, foobar_notification_service_get_popup_notifications( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for the notification service. +// +void foobar_notification_service_finalize( GObject* object ) +{ + FoobarNotificationService* self = (FoobarNotificationService*)object; + + if ( self->skeleton ) { g_dbus_interface_skeleton_unexport( G_DBUS_INTERFACE_SKELETON( self->skeleton ) ); } + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->notifications ) ); ++i ) + { + FoobarNotification* notification = g_list_model_get_item( G_LIST_MODEL( self->notifications ), i ); + notification->service = NULL; + } + + g_clear_object( &self->popup_notifications ); + g_clear_object( &self->sorted_notifications ); + g_clear_object( &self->notifications ); + g_clear_object( &self->skeleton ); + g_clear_handle_id( &self->bus_owner_id, g_bus_unown_name ); + g_clear_pointer( &self->cache_path, g_free ); + + g_mutex_clear( &self->write_cache_mutex ); + + G_OBJECT_CLASS( foobar_notification_service_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new notification service instance. +// +FoobarNotificationService* foobar_notification_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_NOTIFICATION_SERVICE, NULL ); +} + +// +// Get a sorted list of all notifications. +// +GListModel* foobar_notification_service_get_notifications( FoobarNotificationService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( self ), NULL ); + return G_LIST_MODEL( self->sorted_notifications ); +} + +// +// Get a sorted list of all visible notifications (i.e. notifications that are not dismissed). +// +GListModel* foobar_notification_service_get_popup_notifications( FoobarNotificationService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( self ), NULL ); + return G_LIST_MODEL( self->popup_notifications ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called after the process has asynchronously acquired the notification bus. +// +// This is where we can export notification daemon functionality using the corresponding DBus skeleton object. +// +void foobar_notification_service_handle_bus_acquired( + GDBusConnection* connection, + gchar const* name, + gpointer userdata ) +{ + (void)name; + FoobarNotificationService* self = (FoobarNotificationService*)userdata; + + self->skeleton = foobar_notifications_skeleton_new( ); + g_signal_connect( + self->skeleton, + "handle-notify", + G_CALLBACK( foobar_notification_service_handle_notify ), + self ); + g_signal_connect( + self->skeleton, + "handle-close-notification", + G_CALLBACK( foobar_notification_service_handle_close_notification ), + self ); + g_signal_connect( + self->skeleton, + "handle-get-capabilities", + G_CALLBACK( foobar_notification_service_handle_get_capabilities ), + self ); + g_signal_connect( + self->skeleton, + "handle-get-server-information", + G_CALLBACK( foobar_notification_service_handle_get_server_information ), + self ); + + g_autoptr( GError ) error = NULL; + if ( !g_dbus_interface_skeleton_export( + G_DBUS_INTERFACE_SKELETON( self->skeleton ), + connection, + "/org/freedesktop/Notifications", + &error ) ) + { + g_warning( "Unable to export notifications interface: %s", error->message ); + } +} + +// +// Called after the process has failed to acquire the notification bus. +// +void foobar_notification_service_handle_bus_lost( + GDBusConnection* connection, + gchar const* name, + gpointer userdata ) +{ + (void)connection; + (void)name; + (void)userdata; + + g_autofree gchar* other_name = NULL; + g_autoptr( FoobarNotifications ) proxy = foobar_notifications_proxy_new_for_bus_sync( + G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + "org.freedesktop.Notifications", + "/org/freedesktop/Notifications", + NULL, + NULL ); + + if ( proxy ) + { + foobar_notifications_call_get_server_information_sync( + proxy, + &other_name, + NULL, + NULL, + NULL, + NULL, + NULL ); + } + + g_warning( + "Unable to register notification daemon because %s is already running.", + other_name ? other_name : "(unknown)" ); +} + +// +// DBus skeleton callback for the "Notify" method. +// +gboolean foobar_notification_service_handle_notify( + FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + gchar const* app_name, + guint replaces_id, + gchar const* image, + gchar const* summary, + gchar const* body, + gchar const* const* actions, + GVariant* hints, + gint expiration, + gpointer userdata ) +{ + FoobarNotificationService* self = (FoobarNotificationService*)userdata; + + g_autoptr( GDateTime ) time = g_date_time_new_now_local( ); + g_autoptr( FoobarNotification ) notification = foobar_notification_new( self ); + foobar_notification_set_id( notification, replaces_id ? replaces_id : self->next_id++ ); + foobar_notification_set_app_name( notification, app_name ); + foobar_notification_set_summary( notification, summary ); + foobar_notification_set_body( notification, body ); + foobar_notification_set_time( notification, time ); + foobar_notification_set_timeout( notification, expiration != -1 ? expiration : DEFAULT_TIMEOUT ); + if ( image && *image ) { foobar_notification_set_image_from_path( notification, image ); } + + if ( hints ) + { + // Process hints to set some more properties for the notification. + + gsize hint_count = g_variant_n_children( hints ); + for ( gsize i = 0; i < hint_count; ++i ) + { + g_autoptr( GVariant ) hint = g_variant_get_child_value( hints, i ); + g_autoptr( GVariant ) key = g_variant_get_child_value( hint, 0 ); + g_autoptr( GVariant ) value_wrapper = g_variant_get_child_value( hint, 1 ); + g_autoptr( GVariant ) value = g_variant_get_variant( value_wrapper ); + gchar const* key_str = g_variant_get_string( key, NULL ); + + if ( !g_strcmp0( key_str, "desktop-entry" ) ) + { + foobar_notification_set_app_entry( notification, g_variant_get_string( value, NULL ) ); + } + else if ( !g_strcmp0( key_str, "resident" ) ) + { + foobar_notification_set_resident( notification, g_variant_get_boolean( value ) ); + } + else if ( !g_strcmp0( key_str, "transient" ) ) + { + foobar_notification_set_transient( notification, g_variant_get_boolean( value ) ); + } + else if ( !g_strcmp0( key_str, "urgency" ) ) + { + foobar_notification_set_urgency( notification, g_variant_get_byte( value ) ); + } + + if ( !foobar_notification_get_image( notification ) ) + { + if ( !g_strcmp0( key_str, "image-path" ) || + !g_strcmp0( key_str, "image_path" ) ) + { + foobar_notification_set_image_from_path( notification, g_variant_get_string( value, NULL ) ); + } + else if ( !g_strcmp0( key_str, "image-data" ) || + !g_strcmp0( key_str, "image_data" ) || + !g_strcmp0( key_str, "icon_data" ) ) + { + gint32 width, height, row_stride, bits_per_sample, channels; + gboolean has_alpha; + g_variant_get( + value, + "(iiibiiay)", + &width, + &height, + &row_stride, + &has_alpha, + &bits_per_sample, + &channels, + NULL ); + + g_autoptr( GVariant ) data_variant = g_variant_get_child_value( value, 6 ); + gsize data_len; + guint8 const* data = g_variant_get_fixed_array( data_variant, &data_len, sizeof( guint8 ) ); + g_autoptr( GBytes ) bytes = g_bytes_new( data, data_len ); + + g_autoptr( GdkPixbuf ) pixbuf = gdk_pixbuf_new_from_bytes( + bytes, + GDK_COLORSPACE_RGB, + has_alpha, + bits_per_sample, + width, + height, + row_stride ); + foobar_notification_set_image( notification, pixbuf ); + } + } + } + } + + if ( actions ) + { + // Actions are stored in a flat array containing the label followed by the ID of the action. + + while ( *actions ) + { + gchar const* label = *actions++; + gchar const* id = *actions++; + if ( !id ) + { + g_warning( "Action missing id: %s", label ); + break; + } + + g_autoptr( FoobarNotificationAction ) action = foobar_notification_action_new( notification ); + foobar_notification_action_set_id( action, id ); + foobar_notification_action_set_label( action, label ); + foobar_notification_add_action( notification, action ); + } + } + + // Add the notification to the list and start the timeout. + + g_list_store_append( self->notifications, notification ); + foobar_notification_resume_timeout( notification ); + foobar_notification_service_write_cache( self ); + + foobar_notifications_complete_notify( iface, invocation, foobar_notification_get_id( notification ) ); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +// +// DBus skeleton callback for the "CloseNotification" method. +// +gboolean foobar_notification_service_handle_close_notification( + FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + guint id, + gpointer userdata ) +{ + FoobarNotificationService* self = (FoobarNotificationService*)userdata; + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->notifications ) ); ++i ) + { + FoobarNotification* notification = g_list_model_get_item( G_LIST_MODEL( self->notifications ), i ); + if ( foobar_notification_get_id( notification ) == id ) + { + foobar_notification_close( notification ); + break; + } + } + + foobar_notifications_complete_close_notification( iface, invocation ); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +// +// DBus skeleton callback for the "GetCapabilities" method. +// +gboolean foobar_notification_service_handle_get_capabilities( + FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + gpointer userdata ) +{ + (void)userdata; + + gchar const* capabilities[] = + { + "action-icons", + "actions", + "body", + "body-hyperlinks", + "body-markup", + "icon-static", + "persistence", + NULL + }; + foobar_notifications_complete_get_capabilities( iface, invocation, capabilities ); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +// +// DBus skeleton callback for the "GetServerInformation" method. +// +gboolean foobar_notification_service_handle_get_server_information( + FoobarNotifications* iface, + GDBusMethodInvocation* invocation, + gpointer userdata ) +{ + (void)userdata; + + foobar_notifications_complete_get_server_information( + iface, + invocation, + "Foobar", + "Hannes Schulze", + "1.0.0", + "1.2" ); + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Synchronously read the cache file at self->cache_path, populating self->notifications and initializing self->next_id. +// +void foobar_notification_service_read_cache( FoobarNotificationService* self ) +{ + g_autoptr( GError ) error = NULL; + + if ( self->cache_path && g_file_test( self->cache_path, G_FILE_TEST_EXISTS ) ) + { + g_autoptr( JsonParser ) parser = json_parser_new( ); + if ( !json_parser_load_from_mapped_file( parser, self->cache_path, &error ) ) + { + g_warning( "Unable to read cached notifications: %s", error->message ); + return; + } + + JsonNode* root_node = json_parser_get_root( parser ); + JsonArray* root_array = json_node_get_array( root_node ); + for ( guint i = 0; i < json_array_get_length( root_array ); ++i ) + { + JsonObject* notification_object = json_array_get_object_element( root_array, i ); + g_autoptr( FoobarNotification ) notification = foobar_notification_new( self ); + + foobar_notification_set_dismissed( notification, TRUE ); + + guint id = json_object_get_int_member( notification_object, "id" ); + foobar_notification_set_id( notification, id ); + + gchar const* app_entry = json_object_get_string_member_with_default( notification_object, "app-entry", NULL ); + foobar_notification_set_app_entry( notification, app_entry ); + + gchar const* app_name = json_object_get_string_member_with_default( notification_object, "app-name", NULL ); + foobar_notification_set_app_name( notification, app_name ); + + gchar const* body = json_object_get_string_member_with_default( notification_object, "body", NULL ); + foobar_notification_set_body( notification, body ); + + gchar const* summary = json_object_get_string_member_with_default( notification_object, "summary", NULL ); + foobar_notification_set_summary( notification, summary ); + + gchar const* image_path = json_object_get_string_member_with_default( notification_object, "image-path", NULL ); + gchar const* image_data = json_object_get_string_member_with_default( notification_object, "image-data", NULL ); + if ( image_path ) { foobar_notification_set_image_from_path( notification, image_path ); } + else if ( image_data ) { foobar_notification_set_image_from_data( notification, image_data ); } + + gboolean is_resident = json_object_get_boolean_member( notification_object, "is-resident" ); + foobar_notification_set_resident( notification, is_resident ); + + gchar const* time_str = json_object_get_string_member_with_default( notification_object, "time", NULL ); + GTimeZone* tz = g_time_zone_new_local( ); + GDateTime* time = time_str ? g_date_time_new_from_iso8601( time_str, tz ) : NULL; + foobar_notification_set_time( notification, time ); + g_time_zone_unref( tz ); + if ( time ) { g_date_time_unref( time ); } + + gint64 timeout = json_object_get_int_member( notification_object, "timeout" ); + foobar_notification_set_timeout( notification, timeout ); + + FoobarNotificationUrgency urgency = json_object_get_int_member( notification_object, "urgency" ); + foobar_notification_set_urgency( notification, urgency ); + + JsonArray* actions_array = json_object_get_array_member( notification_object, "actions" ); + for ( guint j = 0; j < json_array_get_length( actions_array ); ++j ) + { + JsonObject* action_object = json_array_get_object_element( actions_array, j ); + g_autoptr( FoobarNotificationAction ) action = foobar_notification_action_new( notification ); + + gchar const* action_id = json_object_get_string_member_with_default( action_object, "id", NULL ); + foobar_notification_action_set_id( action, action_id ); + + gchar const* label = json_object_get_string_member_with_default( action_object, "label", NULL ); + foobar_notification_action_set_label( action, label ); + + foobar_notification_add_action( notification, action ); + } + + g_list_store_append( self->notifications, notification ); + self->next_id = MAX( self->next_id, id + 1 ); + } + } +} + +// +// Asynchronously start writing the cache file at self->cache_path. +// +void foobar_notification_service_write_cache( FoobarNotificationService* self ) +{ + guint count = g_list_model_get_n_items( G_LIST_MODEL( self->notifications ) ); + g_autoptr( GPtrArray ) notifications = g_ptr_array_new_full( count, g_object_unref ); + for ( guint i = 0; i < count; ++i ) + { + FoobarNotification* notification = g_list_model_get_item( G_LIST_MODEL( self->notifications ), i ); + if ( !foobar_notification_is_transient( notification ) ) + { + g_ptr_array_add( notifications, g_object_ref( notification ) ); + } + } + + foobar_notification_service_write_cache_async( + self, + g_steal_pointer( ¬ifications ), + NULL, + foobar_notification_service_write_cache_cb, + NULL ); +} + +// +// Callback invoked when the cache was written successfully or failed. +// +void foobar_notification_service_write_cache_cb( + GObject* object, + GAsyncResult* result, + gpointer userdata ) +{ + (void)userdata; + FoobarNotificationService* self = (FoobarNotificationService*)object; + + g_autoptr( GError ) error = NULL; + if ( !foobar_notification_service_write_cache_finish( self, result, &error ) ) + { + g_warning( "Unable to write cached notifications: %s", error->message ); + } +} + +// +// Asynchronously write the cache file at self->cache_path, using the provided snapshot of the list. +// +void foobar_notification_service_write_cache_async( + FoobarNotificationService* self, + GPtrArray* notifications, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer userdata ) +{ + g_autoptr( GTask ) task = g_task_new( self, cancellable, callback, userdata ); + g_task_set_name( task, "write-notification-cache" ); + g_task_set_task_data( task, notifications, (GDestroyNotify)g_ptr_array_unref ); + g_task_run_in_thread( task, foobar_notification_service_write_cache_thread ); +} + +// +// Get the asynchronous result for writing the cache file, returning TRUE on success, or FALSE on error. +// +gboolean foobar_notification_service_write_cache_finish( + FoobarNotificationService* self, + GAsyncResult* result, + GError** error ) +{ + (void)self; + + return g_task_propagate_boolean( G_TASK( result ), error ); +} + +// +// Task implementation for foobar_notification_service_write_cache_async, invoked on a background thread. +// +void foobar_notification_service_write_cache_thread( + GTask* task, + gpointer source_object, + gpointer task_data, + GCancellable* cancellable ) +{ + (void)cancellable; + FoobarNotificationService* self = (FoobarNotificationService*)source_object; + GPtrArray* notifications = (GPtrArray*)task_data; + + if ( !self->cache_path ) + { + g_task_return_boolean( task, TRUE ); + return; + } + + g_autoptr( GError ) error = NULL; + + g_autoptr( JsonBuilder ) builder = json_builder_new( ); + json_builder_begin_array( builder ); + for ( guint i = 0; i < notifications->len; ++i ) + { + // Only init-only properties are read here so no extra synchronization is needed. + + FoobarNotification* notification = g_ptr_array_index( notifications, i ); + + json_builder_begin_object( builder ); + + json_builder_set_member_name( builder, "id" ); + json_builder_add_int_value( builder, foobar_notification_get_id( notification ) ); + + json_builder_set_member_name( builder, "app-entry" ); + json_builder_add_string_value( builder, foobar_notification_get_app_entry( notification ) ); + + json_builder_set_member_name( builder, "app-name" ); + json_builder_add_string_value( builder, foobar_notification_get_app_name( notification ) ); + + json_builder_set_member_name( builder, "body" ); + json_builder_add_string_value( builder, foobar_notification_get_body( notification ) ); + + json_builder_set_member_name( builder, "summary" ); + json_builder_add_string_value( builder, foobar_notification_get_summary( notification ) ); + + json_builder_set_member_name( builder, "image-path" ); + json_builder_add_string_value( builder, foobar_notification_get_image_path( notification ) ); + + json_builder_set_member_name( builder, "image-data" ); + json_builder_add_string_value( builder, foobar_notification_get_image_data( notification ) ); + + json_builder_set_member_name( builder, "is-resident" ); + json_builder_add_boolean_value( builder, foobar_notification_is_resident( notification ) ); + + GDateTime* time = foobar_notification_get_time( notification ); + gchar* time_str = time ? g_date_time_format_iso8601( time ) : NULL; + json_builder_set_member_name( builder, "time" ); + json_builder_add_string_value( builder, time_str ); + g_free( time_str ); + + json_builder_set_member_name( builder, "timeout" ); + json_builder_add_int_value( builder, foobar_notification_get_timeout( notification ) ); + + json_builder_set_member_name( builder, "urgency" ); + json_builder_add_int_value( builder, foobar_notification_get_urgency( notification ) ); + + guint action_count; + FoobarNotificationAction** actions = foobar_notification_get_actions( notification, &action_count ); + json_builder_set_member_name( builder, "actions" ); + json_builder_begin_array( builder ); + for ( guint j = 0; j < action_count; ++j ) + { + FoobarNotificationAction* action = actions[j]; + + json_builder_begin_object( builder ); + + json_builder_set_member_name( builder, "id" ); + json_builder_add_string_value( builder, foobar_notification_action_get_id( action ) ); + + json_builder_set_member_name( builder, "label" ); + json_builder_add_string_value( builder, foobar_notification_action_get_label( action ) ); + + json_builder_end_object( builder ); + } + json_builder_end_array( builder ); + + json_builder_end_object( builder ); + } + json_builder_end_array( builder ); + + g_autoptr( JsonNode ) root_node = json_builder_get_root( builder ); + g_autoptr( JsonGenerator ) generator = json_generator_new( ); + json_generator_set_root( generator, root_node ); + + g_mutex_lock( &self->write_cache_mutex ); + gboolean success = json_generator_to_file( generator, self->cache_path, &error ); + g_mutex_unlock( &self->write_cache_mutex ); + if ( !success ) + { + g_task_return_error( task, g_steal_pointer( &error ) ); + return; + } + + g_task_return_boolean( task, TRUE ); +} + +// +// Filtering callback for the list of pop-up notifications. +// +gboolean foobar_notification_service_popup_filter_func( + gpointer item, + gpointer userdata ) +{ + (void)userdata; + + FoobarNotification* notification = item; + return !foobar_notification_is_dismissed( notification ); +} + +// +// Sorting callback for the list of notifications. +// +// Notifications are sorted by timestamp in descending order. +// +gint foobar_notification_service_sort_func( + gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ) +{ + (void)userdata; + + FoobarNotification* notification_a = (FoobarNotification*)item_a; + FoobarNotification* notification_b = (FoobarNotification*)item_b; + return -g_date_time_compare( + foobar_notification_get_time( notification_a ), + foobar_notification_get_time( notification_b ) ); +} diff --git a/csrc/services/notification-service.h b/csrc/services/notification-service.h new file mode 100644 index 0000000..abcca4e --- /dev/null +++ b/csrc/services/notification-service.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_NOTIFICATION_URGENCY foobar_notification_urgency_get_type( ) +#define FOOBAR_TYPE_NOTIFICATION_ACTION foobar_notification_action_get_type( ) +#define FOOBAR_TYPE_NOTIFICATION foobar_notification_get_type( ) +#define FOOBAR_TYPE_NOTIFICATION_SERVICE foobar_notification_service_get_type( ) + +typedef enum +{ + // reminder to myself that i should have studied this shit instead of writing a fucking bar in c + FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_COMFORT = 0, + FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY = 1, + FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_LIFE = 2, +} FoobarNotificationUrgency; + +GType foobar_notification_urgency_get_type( void ); + +G_DECLARE_FINAL_TYPE( FoobarNotificationAction, foobar_notification_action, FOOBAR, NOTIFICATION_ACTION, GObject ) + +gchar const* foobar_notification_action_get_id ( FoobarNotificationAction* self ); +gchar const* foobar_notification_action_get_label( FoobarNotificationAction* self ); +void foobar_notification_action_invoke ( FoobarNotificationAction* self ); + +G_DECLARE_FINAL_TYPE( FoobarNotification, foobar_notification, FOOBAR, NOTIFICATION, GObject ) + +guint foobar_notification_get_id ( FoobarNotification* self ); +FoobarNotificationAction** foobar_notification_get_actions ( FoobarNotification* self, + guint* out_count ); +gchar const* foobar_notification_get_app_entry ( FoobarNotification* self ); +gchar const* foobar_notification_get_app_name ( FoobarNotification* self ); +gchar const* foobar_notification_get_body ( FoobarNotification* self ); +gchar const* foobar_notification_get_summary ( FoobarNotification* self ); +GdkPixbuf* foobar_notification_get_image ( FoobarNotification* self ); +gboolean foobar_notification_is_dismissed ( FoobarNotification* self ); +gboolean foobar_notification_is_resident ( FoobarNotification* self ); +gboolean foobar_notification_is_transient ( FoobarNotification* self ); +GDateTime* foobar_notification_get_time ( FoobarNotification* self ); +gint64 foobar_notification_get_timeout ( FoobarNotification* self ); +FoobarNotificationUrgency foobar_notification_get_urgency ( FoobarNotification* self ); +void foobar_notification_block_timeout ( FoobarNotification* self ); +void foobar_notification_resume_timeout( FoobarNotification* self ); +void foobar_notification_dismiss ( FoobarNotification* self ); +void foobar_notification_close ( FoobarNotification* self ); + +G_DECLARE_FINAL_TYPE( FoobarNotificationService, foobar_notification_service, FOOBAR, NOTIFICATION_SERVICE, GObject ) + +FoobarNotificationService* foobar_notification_service_new ( void ); +GListModel* foobar_notification_service_get_notifications ( FoobarNotificationService* self ); +GListModel* foobar_notification_service_get_popup_notifications( FoobarNotificationService* self ); + +G_END_DECLS diff --git a/csrc/services/quick-answer-service.c b/csrc/services/quick-answer-service.c new file mode 100644 index 0000000..670a72f --- /dev/null +++ b/csrc/services/quick-answer-service.c @@ -0,0 +1,360 @@ +#include "services/quick-answer-service.h" +#include "services/quick-answers/math.h" +#include "launcher-item.h" +#include + +// +// FoobarQuickAnswer: +// +// A launcher item representing a quick, one-line answer to the query entered by the user. +// + +struct _FoobarQuickAnswer +{ + GObject parent_instance; + gchar* title; + gchar* value; + GIcon* icon; +}; + +enum +{ + ANSWER_PROP_VALUE = 1, + ANSWER_PROP_TITLE, + ANSWER_PROP_DESCRIPTION, + ANSWER_PROP_ICON, + N_ANSWER_PROPS, +}; + +static GParamSpec* answer_props[N_ANSWER_PROPS] = { 0 }; + +static void foobar_quick_answer_class_init ( FoobarQuickAnswerClass* klass ); +static void foobar_quick_answer_launcher_item_interface_init( FoobarLauncherItemInterface* iface ); +static void foobar_quick_answer_init ( FoobarQuickAnswer* self ); +static void foobar_quick_answer_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_quick_answer_finalize ( GObject* object ); +static FoobarQuickAnswer* foobar_quick_answer_new ( void ); +static gchar const* foobar_quick_answer_get_title ( FoobarLauncherItem* self ); +static gchar const* foobar_quick_answer_get_description ( FoobarLauncherItem* self ); +static GIcon* foobar_quick_answer_get_icon ( FoobarLauncherItem* self ); +static void foobar_quick_answer_activate ( FoobarLauncherItem* self ); +static void foobar_quick_answer_set_title ( FoobarQuickAnswer* self, + gchar const* value ); +static void foobar_quick_answer_set_value ( FoobarQuickAnswer* self, + gchar const* value ); +static void foobar_quick_answer_set_icon ( FoobarQuickAnswer* self, + GIcon* value ); + +G_DEFINE_FINAL_TYPE_WITH_CODE( + FoobarQuickAnswer, + foobar_quick_answer, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE( FOOBAR_TYPE_LAUNCHER_ITEM, foobar_quick_answer_launcher_item_interface_init ) ) + +// +// FoobarQuickAnswerService: +// +// Service providing quick answers for search queries in the launcher (if available). This includes evaluating +// mathematical expressions. +// + +struct _FoobarQuickAnswerService +{ + GObject parent_instance; +}; + +static void foobar_quick_answer_service_class_init( FoobarQuickAnswerServiceClass* klass ); +static void foobar_quick_answer_service_init ( FoobarQuickAnswerService* self ); +static FoobarQuickAnswer* foobar_quick_answer_service_query_math( gchar const* query ); + +G_DEFINE_FINAL_TYPE( FoobarQuickAnswerService, foobar_quick_answer_service, G_TYPE_OBJECT ) + +// --------------------------------------------------------------------------------------------------------------------- +// Quick Answers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for quick answers. +// +void foobar_quick_answer_class_init( FoobarQuickAnswerClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_quick_answer_get_property; + object_klass->finalize = foobar_quick_answer_finalize; + + gpointer launcher_item_iface = g_type_default_interface_peek( FOOBAR_TYPE_LAUNCHER_ITEM ); + answer_props[ANSWER_PROP_VALUE] = g_param_spec_string( + "value", + "Value", + "The raw value of the quick answer.", + NULL, + G_PARAM_READABLE ); + answer_props[ANSWER_PROP_TITLE] = g_param_spec_override( + "title", + g_object_interface_find_property( launcher_item_iface, "title" ) ); + answer_props[ANSWER_PROP_DESCRIPTION] = g_param_spec_override( + "description", + g_object_interface_find_property( launcher_item_iface, "description" ) ); + answer_props[ANSWER_PROP_ICON] = g_param_spec_override( + "icon", + g_object_interface_find_property( launcher_item_iface, "icon" ) ); + g_object_class_install_properties( object_klass, N_ANSWER_PROPS, answer_props ); +} + +// +// Static initialization of the FoobarLauncherItem interface. +// +void foobar_quick_answer_launcher_item_interface_init( FoobarLauncherItemInterface* iface ) +{ + iface->get_title = foobar_quick_answer_get_title; + iface->get_description = foobar_quick_answer_get_description; + iface->get_icon = foobar_quick_answer_get_icon; + iface->activate = foobar_quick_answer_activate; +} + +// +// Instance initialization for quick answers. +// +void foobar_quick_answer_init( FoobarQuickAnswer* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_quick_answer_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarQuickAnswer* self = (FoobarQuickAnswer*)object; + + switch ( prop_id ) + { + case ANSWER_PROP_VALUE: + g_value_set_string( value, foobar_quick_answer_get_value( self ) ); + break; + case ANSWER_PROP_TITLE: + g_value_set_string( value, foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM ( self ) ) ); + break; + case ANSWER_PROP_DESCRIPTION: + g_value_set_string( value, foobar_launcher_item_get_description( FOOBAR_LAUNCHER_ITEM( self ) ) ); + break; + case ANSWER_PROP_ICON: + g_value_set_object( value, foobar_launcher_item_get_icon( FOOBAR_LAUNCHER_ITEM( self ) ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for quick answers. +// +void foobar_quick_answer_finalize( GObject* object ) +{ + FoobarQuickAnswer* self = (FoobarQuickAnswer*)object; + + g_clear_pointer( &self->title, g_free ); + g_clear_pointer( &self->value, g_free ); + g_clear_object( &self->icon ); + + G_OBJECT_CLASS( foobar_quick_answer_parent_class )->finalize( object ); +} + +// +// Create a new, default-initialized quick answer. +// +FoobarQuickAnswer* foobar_quick_answer_new( void ) +{ + return g_object_new( FOOBAR_TYPE_QUICK_ANSWER, NULL ); +} + +// +// Get the raw value of the quick answer. +// +gchar const* foobar_quick_answer_get_value( FoobarQuickAnswer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_QUICK_ANSWER( self ), NULL ); + return self->value; +} + +// +// Get the title for the quick answer. +// +gchar const* foobar_quick_answer_get_title( FoobarLauncherItem* item ) +{ + FoobarQuickAnswer* self = ( FoobarQuickAnswer* )item; + return self->title; +} + +// +// Get the description shown below the title, always NULL for quick answers. +// +gchar const* foobar_quick_answer_get_description( FoobarLauncherItem* item ) +{ + (void)item; + return NULL; +} + +// +// Get an icon representing the quick answer. +// +GIcon* foobar_quick_answer_get_icon( FoobarLauncherItem* item ) +{ + FoobarQuickAnswer* self = ( FoobarQuickAnswer* )item; + return self->icon; +} + +// +// Copy the quick answer's value into the clipboard. +// +void foobar_quick_answer_activate( FoobarLauncherItem* item ) +{ + FoobarQuickAnswer* self = ( FoobarQuickAnswer* )item; + if ( self->value ) + { + GdkClipboard* clipboard = gdk_display_get_clipboard( gdk_display_get_default( ) ); + gdk_clipboard_set_text( clipboard, self->value ); + } +} + +// +// Update the raw value of the quick answer. +// +void foobar_quick_answer_set_value( + FoobarQuickAnswer* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_QUICK_ANSWER( self ) ); + + if ( g_strcmp0( self->value, value ) ) + { + g_clear_pointer( &self->value, g_free ); + self->value = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), answer_props[ANSWER_PROP_VALUE] ); + } +} + +// +// Update the title for the quick answer. +// +void foobar_quick_answer_set_title( + FoobarQuickAnswer* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_QUICK_ANSWER( self ) ); + + if ( g_strcmp0( self->title, value ) ) + { + g_clear_pointer( &self->title, g_free ); + self->title = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), answer_props[ANSWER_PROP_TITLE] ); + } +} + +// +// Update the icon representing the quick answer. +// +void foobar_quick_answer_set_icon( + FoobarQuickAnswer* self, + GIcon* value ) +{ + g_return_if_fail( FOOBAR_IS_QUICK_ANSWER( self ) ); + + if ( self->icon != value ) + { + g_clear_object( &self->icon ); + self->icon = value ? g_object_ref( value ) : NULL; + g_object_notify_by_pspec( G_OBJECT( self ), answer_props[ANSWER_PROP_ICON] ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the quick answer service. +// +void foobar_quick_answer_service_class_init( FoobarQuickAnswerServiceClass* klass ) +{ + (void)klass; +} + +// +// Instance initialization for the quick answer service. +// +void foobar_quick_answer_service_init( FoobarQuickAnswerService* self ) +{ + (void)self; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new quick answer service instance. +// +FoobarQuickAnswerService* foobar_quick_answer_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_QUICK_ANSWER_SERVICE, NULL ); +} + +// +// Query the service for a quick answer. +// +FoobarQuickAnswer* foobar_quick_answer_service_query( + FoobarQuickAnswerService* self, + gchar const* query ) +{ + (void)self; + + FoobarQuickAnswer* result = foobar_quick_answer_service_query_math( query ); + if ( result ) { return result; } + + return NULL; +} + +// +// Try to parse the query as a mathematical expression and evaluate it. +// +FoobarQuickAnswer* foobar_quick_answer_service_query_math( gchar const* query ) +{ + FoobarQuickAnswer* result = NULL; + + gsize token_count; + g_autofree FoobarMathToken* tokens = foobar_math_lex( query, strlen( query ), &token_count ); + if ( tokens ) + { + FoobarMathExpression* expr = foobar_math_parse( tokens, token_count ); + if ( expr ) + { + // foobar_math_expression_print( expr, 0 ); + + FoobarMathValue val = { 0 }; + if ( foobar_math_evaluate( expr, &val ) ) + { + g_autofree gchar* value = foobar_math_value_to_string( val ); + g_autofree gchar* title = g_strdup_printf( "= %s", value ); + g_autoptr( GIcon ) icon = g_themed_icon_new( "fluent-calculator-symbolic" ); + result = foobar_quick_answer_new( ); + foobar_quick_answer_set_value( result, value ); + foobar_quick_answer_set_title( result, title ); + foobar_quick_answer_set_icon( result, icon ); + foobar_math_value_free( val ); + } + + foobar_math_expression_free( expr ); + } + } + + return result; +} diff --git a/csrc/services/quick-answer-service.h b/csrc/services/quick-answer-service.h new file mode 100644 index 0000000..adb0701 --- /dev/null +++ b/csrc/services/quick-answer-service.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_QUICK_ANSWER foobar_quick_answer_get_type( ) +#define FOOBAR_TYPE_QUICK_ANSWER_SERVICE foobar_quick_answer_service_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarQuickAnswer, foobar_quick_answer, FOOBAR, QUICK_ANSWER, GObject ) + +gchar const* foobar_quick_answer_get_value( FoobarQuickAnswer* self ); + +G_DECLARE_FINAL_TYPE( FoobarQuickAnswerService, foobar_quick_answer_service, FOOBAR, QUICK_ANSWER_SERVICE, GObject ) + +FoobarQuickAnswerService* foobar_quick_answer_service_new ( void ); +FoobarQuickAnswer* foobar_quick_answer_service_query ( FoobarQuickAnswerService* self, + gchar const* query ); + +G_END_DECLS diff --git a/csrc/services/quick-answers/math-lexer.c b/csrc/services/quick-answers/math-lexer.c new file mode 100644 index 0000000..7fbe877 --- /dev/null +++ b/csrc/services/quick-answers/math-lexer.c @@ -0,0 +1,248 @@ +#include "services/quick-answers/math.h" + +// +// FoobarMathToken: +// +// A token in a mathematical expression. The token contents are left as-is and need not be null-terminated. For example, +// "sin(pi) - 3" would be represented as: +// - IDENTIFIER: "sin" +// - PAREN_OPEN: "(" +// - IDENTIFIER: "pi" +// - PAREN_CLOSE: ")" +// - MINUS: "-" +// - NUMBER: "3" +// + +// +// Lexer: +// +// The lexer context/state which is passed around. +// + +typedef struct _Lexer Lexer; + +struct _Lexer +{ + GArray* result; + gchar const* input; + gsize input_length; + gsize position; + gsize token_start; +}; + +static gboolean char_is_whitespace( char c ); +static gboolean char_is_digit ( char c ); +static gboolean char_is_identifier( char c ); + +static char lexer_peek ( Lexer const* ctx, + gsize offset ); +static char lexer_pop ( Lexer* ctx ); +static void lexer_begin_token ( Lexer* ctx ); +static void lexer_commit_token ( Lexer* ctx, + FoobarMathTokenType type ); +static void lexer_commit_single( Lexer* ctx, + FoobarMathTokenType type ); + +// --------------------------------------------------------------------------------------------------------------------- +// Lexing +// --------------------------------------------------------------------------------------------------------------------- + +// +// Divide an input string into a list of tokens. +// +// On success, this will return a newly allocated array of tokens which should be freed using g_free. The number of +// elements will be written into out_count. +// +// On error, this will return NULL. +// +FoobarMathToken* foobar_math_lex( + gchar const* input, + gsize input_length, + gsize* out_count ) +{ + g_autoptr( GArray ) result = g_array_new( FALSE, TRUE, sizeof( FoobarMathToken ) ); + + Lexer ctx = { 0 }; + ctx.result = result; + ctx.input = input; + ctx.input_length = input_length; + + while ( ctx.position < input_length ) + { + if ( char_is_whitespace( lexer_peek( &ctx, 0 ) ) ) + { + // Skip whitespace. + + lexer_pop( &ctx ); + } + else if ( char_is_digit( lexer_peek( &ctx, 0 ) ) ) + { + // Process numbers as a whole, don't allow whitespace inbetween. + + lexer_begin_token( &ctx ); + while ( char_is_digit( lexer_peek( &ctx, 0 ) ) ) + { + lexer_pop( &ctx ); + } + + if ( lexer_peek( &ctx, 0 ) == '.' && char_is_digit( lexer_peek( &ctx, 1 ) ) ) + { + lexer_pop( &ctx ); + while ( char_is_digit( lexer_peek( &ctx, 0 ) ) ) + { + lexer_pop( &ctx ); + } + } + + lexer_commit_token( &ctx, FOOBAR_MATH_TOKEN_NUMBER ); + } + else if ( char_is_identifier( lexer_peek( &ctx, 0 ) ) ) + { + // Don't validate identifiers, leave that to the parser. + + lexer_begin_token( &ctx ); + while ( char_is_identifier( lexer_peek( &ctx, 0 ) ) ) + { + lexer_pop( &ctx ); + } + lexer_commit_token( &ctx, FOOBAR_MATH_TOKEN_IDENTIFIER ); + } + else if ( lexer_peek( &ctx, 0 ) == '(' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PAREN_OPEN ); + } + else if ( lexer_peek( &ctx, 0 ) == ')' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PAREN_CLOSE ); + } + else if ( lexer_peek( &ctx, 0 ) == '+' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PLUS ); + } + else if ( lexer_peek( &ctx, 0 ) == '-' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_MINUS ); + } + else if ( lexer_peek( &ctx, 0 ) == '*' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_MUL ); + } + else if ( lexer_peek( &ctx, 0 ) == '/' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_DIV ); + } + else if ( lexer_peek( &ctx, 0 ) == '^' ) + { + lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_POW ); + } + else + { + // Unknown character. + + return NULL; + } + } + + return g_array_steal( result, out_count ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Character Classification +// --------------------------------------------------------------------------------------------------------------------- + +// +// Check if a character is a whitespace symbol (space, tab, carriage return). +// +gboolean char_is_whitespace( char c ) +{ + return c == ' ' || c == '\t' || c == '\r'; +} + +// +// Check if a character is a decimal digit. +// +gboolean char_is_digit( char c ) +{ + return c >= '0' && c <= '9'; +} + +// +// Check if a character belongs to an identifier (like "sin", "pi", etc.). +// +gboolean char_is_identifier( char c ) +{ + // Identifiers are all lowercase ASCII. + + return c >= 'a' && c <= 'z'; +} + +// --------------------------------------------------------------------------------------------------------------------- +// State Helpers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Peek at the current character without consuming it. +// +// If this is the end of the input string, return 0. +// +char lexer_peek( + Lexer const* ctx, + gsize offset ) +{ + gsize position = ctx->position + offset; + if ( position >= ctx->input_length ) { return 0; } + + return ctx->input[position]; +} + +// +// Consume the current character, moving forward to the next one. +// +// If this is the end of the input string, return 0. +// +char lexer_pop( Lexer* ctx ) +{ + if ( ctx->position >= ctx->input_length ) { return 0; } + + return ctx->input[ctx->position++]; +} + +// +// Start recording a token by remembering the current position. +// +// The current character will be the first one in the token. +// +void lexer_begin_token( Lexer* ctx ) +{ + ctx->token_start = ctx->position; +} + +// +// Stop recording a token, saving the string between the call to lexer_begin_token as the specified type. +// +// The current character will not be a part of the token. +// +void lexer_commit_token( + Lexer* ctx, + FoobarMathTokenType type ) +{ + FoobarMathToken token = { 0 }; + token.data = &ctx->input[ctx->token_start]; + token.length = ctx->position - ctx->token_start; + token.type = type; + g_array_append_val( ctx->result, token ); +} + +// +// Consume the current character and record it as a token. +// +// This is equivalent to calling lexer_begin_token, lexer_pop, and lexer_commit_token. +// +void lexer_commit_single( + Lexer* ctx, + FoobarMathTokenType type ) +{ + lexer_begin_token( ctx ); + lexer_pop( ctx ); + lexer_commit_token( ctx, type ); +} diff --git a/csrc/services/quick-answers/math-parser.c b/csrc/services/quick-answers/math-parser.c new file mode 100644 index 0000000..17d976d --- /dev/null +++ b/csrc/services/quick-answers/math-parser.c @@ -0,0 +1,414 @@ +#include "services/quick-answers/math.h" + +// +// Parser: +// +// The parser context/state which is passed around. +// + +typedef struct _Parser Parser; + +struct _Parser +{ + FoobarMathToken const* tokens; + gsize tokens_count; + gsize position; +}; + +// +// InfixPrecedence: +// +// Value indicating how strongly infix operators should bind. The higher the value, the stronger they should bind. +// + +typedef enum +{ + INFIX_PRECEDENCE_INVALID = -1, + INFIX_PRECEDENCE_NONE = 0, + INFIX_PRECEDENCE_ADDITIVE = 1, + INFIX_PRECEDENCE_MULTIPLICATIVE = 2, + INFIX_PRECEDENCE_EXPONENTS = 3, +} InfixPrecedence; + +static FoobarMathExpression* parser_process ( Parser* ctx, + FoobarMathExpression* lhs, + InfixPrecedence min_precedence ); +static FoobarMathExpression* parser_process_single ( Parser* ctx, + gboolean allow_sign ); +static FoobarMathExpression* parser_process_identifier ( Parser* ctx ); +static FoobarMathExpression* parser_process_number ( Parser* ctx ); +static FoobarMathExpression* parser_process_function ( Parser* ctx, + FoobarMathFunction function ); +static FoobarMathExpression* parser_process_parens ( Parser* ctx ); +static FoobarMathExpression* parser_process_sign ( Parser* ctx ); +static FoobarMathToken const* parser_peek ( Parser const* ctx ); +static FoobarMathToken const* parser_pop ( Parser* ctx ); +static gboolean parser_match_identifier ( FoobarMathToken const* token, + gchar const* identifier ); +static InfixPrecedence parser_operator_precedence( FoobarMathToken const* token ); +static FoobarMathOperation parser_operator ( FoobarMathToken const* token ); + +// --------------------------------------------------------------------------------------------------------------------- +// Parsing +// --------------------------------------------------------------------------------------------------------------------- + +// +// Transform a sequence of tokens into a mathematical expression. +// +// On success, this will return a newly allocated expression which should be freed using foobar_math_expression_free. +// +// On error, this will return NULL. +// +FoobarMathExpression* foobar_math_parse( + FoobarMathToken const* tokens, + gsize tokens_count ) +{ + Parser ctx = { 0 }; + ctx.tokens = tokens; + ctx.tokens_count = tokens_count; + + FoobarMathExpression* lhs = parser_process_single( &ctx, TRUE ); + if ( !lhs ) { return NULL; } + + FoobarMathExpression* expr = parser_process( &ctx, lhs, INFIX_PRECEDENCE_NONE ); + if ( !expr ) { return NULL; } + + if ( parser_peek( &ctx ) ) + { + foobar_math_expression_free( expr ); + return NULL; + } + + return expr; +} + +// +// Process tokens until the end of the current precedence scope (indicated by min_precedence), starting at an operator. +// +FoobarMathExpression* parser_process( + Parser* ctx, + FoobarMathExpression* lhs, + InfixPrecedence min_precedence ) +{ + FoobarMathToken const* op = parser_peek( ctx ); + InfixPrecedence op_precedence = parser_operator_precedence( op ); + while ( op_precedence >= min_precedence ) + { + parser_pop( ctx ); + + FoobarMathExpression* rhs = parser_process_single( ctx, FALSE ); + if ( !rhs ) + { + foobar_math_expression_free( lhs ); + return NULL; + } + + FoobarMathToken const* next_op = parser_peek( ctx ); + while ( parser_operator_precedence( next_op ) > op_precedence ) + { + rhs = parser_process( ctx, rhs, op_precedence + 1 ); + if ( !rhs ) + { + foobar_math_expression_free( lhs ); + return NULL; + } + + next_op = parser_peek( ctx ); + } + + lhs = foobar_math_expression_new_operation( parser_operator( op ), lhs, rhs ); + + op = parser_peek( ctx ); + op_precedence = parser_operator_precedence( op ); + } + + return lhs; +} + +// +// Process a single value which may be either a constant, function, number, expression enclosed in parenthesis or +// negation. +// +// If an unexpected token is encountered (i.e. an operator), this will return FALSE. +// +FoobarMathExpression* parser_process_single( + Parser* ctx, + gboolean allow_sign ) +{ + FoobarMathToken const* token = parser_peek( ctx ); + if ( !token ) { return NULL; } + + switch ( token->type ) + { + case FOOBAR_MATH_TOKEN_IDENTIFIER: + return parser_process_identifier( ctx ); + case FOOBAR_MATH_TOKEN_NUMBER: + return parser_process_number( ctx ); + case FOOBAR_MATH_TOKEN_PAREN_OPEN: + return parser_process_parens( ctx ); + case FOOBAR_MATH_TOKEN_MINUS: + case FOOBAR_MATH_TOKEN_PLUS: + if ( !allow_sign ) { return NULL; } + return parser_process_sign( ctx ); + case FOOBAR_MATH_TOKEN_PAREN_CLOSE: + case FOOBAR_MATH_TOKEN_MUL: + case FOOBAR_MATH_TOKEN_DIV: + case FOOBAR_MATH_TOKEN_POW: + return NULL; + default: + g_warn_if_reached( ); + return NULL; + } +} + +// +// Parse an identifier. +// +// If the identifier is a function, this will expected parenthesis and an expression afterwards. +// +FoobarMathExpression* parser_process_identifier( Parser* ctx ) +{ + FoobarMathToken const* token = parser_pop( ctx ); + if ( parser_match_identifier( token, "pi" ) ) + { + return foobar_math_expression_new_constant( FOOBAR_MATH_CONSTANT_PI ); + } + else if ( parser_match_identifier( token, "e" ) ) + { + return foobar_math_expression_new_constant( FOOBAR_MATH_CONSTANT_E ); + } + else if ( parser_match_identifier( token, "exp" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_EXP ); + } + else if ( parser_match_identifier( token, "sqrt" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SQRT ); + } + else if ( parser_match_identifier( token, "sin" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SIN ); + } + else if ( parser_match_identifier( token, "cos" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_COS ); + } + else if ( parser_match_identifier( token, "sec" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SEC ); + } + else if ( parser_match_identifier( token, "csc" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_CSC ); + } + else if ( parser_match_identifier( token, "tan" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_TAN ); + } + else if ( parser_match_identifier( token, "cot" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_COT ); + } + else if ( parser_match_identifier( token, "arcsin" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCSIN ); + } + else if ( parser_match_identifier( token, "arccos" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCOS ); + } + else if ( parser_match_identifier( token, "arcsec" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCSEC ); + } + else if ( parser_match_identifier( token, "arccsc" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCSC ); + } + else if ( parser_match_identifier( token, "arctan" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCTAN ); + } + else if ( parser_match_identifier( token, "arccot" ) ) + { + return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCOT ); + } + else + { + return NULL; + } +} + +// +// Process a numeric value. +// +FoobarMathExpression* parser_process_number( Parser* ctx ) +{ + FoobarMathToken const* token = parser_pop( ctx ); + FoobarMathValue value = { 0 }; + if ( !foobar_math_value_from_string( token->data, token->length, &value ) ) + { + return NULL; + } + + return foobar_math_expression_new_value( value ); +} + +// +// Process a function's parameter which should be enclosed in parenthesis. +// +FoobarMathExpression* parser_process_function( + Parser* ctx, + FoobarMathFunction function ) +{ + FoobarMathToken const* opening_paren = parser_peek( ctx ); + if ( !opening_paren || opening_paren->type != FOOBAR_MATH_TOKEN_PAREN_OPEN ) + { + return NULL; + } + + FoobarMathExpression* arg = parser_process_parens( ctx ); + if ( !arg ) { return NULL; } + + return foobar_math_expression_new_function( function, arg ); +} + +// +// Process an expression enclosed in parenthesis. +// +FoobarMathExpression* parser_process_parens( Parser* ctx ) +{ + parser_pop( ctx ); + FoobarMathExpression* lhs = parser_process_single( ctx, TRUE ); + if ( !lhs ) { return NULL; } + FoobarMathExpression* result = parser_process( ctx, lhs, INFIX_PRECEDENCE_NONE ); + if ( !result ) { return NULL; } + FoobarMathToken const* closing_paren = parser_pop( ctx ); + if ( !closing_paren || closing_paren->type != FOOBAR_MATH_TOKEN_PAREN_CLOSE ) + { + foobar_math_expression_free( result ); + return NULL; + } + return result; +} + +// +// Process a sign (- or +) which may be used for negation of an expression. +// +FoobarMathExpression* parser_process_sign( Parser* ctx ) +{ + FoobarMathToken const* token = parser_pop( ctx ); + FoobarMathExpression* result = parser_process_single( ctx, FALSE ); + if ( !result ) { return NULL; } + + if ( token->type == FOOBAR_MATH_TOKEN_MINUS ) + { + return foobar_math_expression_new_function( FOOBAR_MATH_FUNCTION_NEGATE, result ); + } + else + { + return result; + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// State Helpers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Peek at the current token without consuming it. +// +// If this is the end of the token list, return NULL. +// +FoobarMathToken const* parser_peek( Parser const* ctx ) +{ + if ( ctx->position >= ctx->tokens_count ) { return NULL; } + + return &ctx->tokens[ctx->position]; +} + +// +// Consume the current token. +// +// If this is the end of the token list, return NULL. +// +FoobarMathToken const* parser_pop( Parser* ctx ) +{ + if ( ctx->position >= ctx->tokens_count ) { return NULL; } + + return &ctx->tokens[ctx->position++]; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Match a token's string against the given identifier string. +// +// This is necessary because the token's content is not a null-terminated string. +// +gboolean parser_match_identifier( + FoobarMathToken const* token, + gchar const* identifier ) +{ + if ( strlen( identifier ) != token->length ) { return FALSE; } + + return !strncmp( token->data, identifier, token->length ); +} + +// +// Get the precedence of an infix operator token or INVALID. +// +InfixPrecedence parser_operator_precedence( FoobarMathToken const* token ) +{ + if ( token ) + { + switch ( token->type ) + { + case FOOBAR_MATH_TOKEN_PLUS: + case FOOBAR_MATH_TOKEN_MINUS: + return INFIX_PRECEDENCE_ADDITIVE; + case FOOBAR_MATH_TOKEN_MUL: + case FOOBAR_MATH_TOKEN_DIV: + return INFIX_PRECEDENCE_MULTIPLICATIVE; + case FOOBAR_MATH_TOKEN_POW: + return INFIX_PRECEDENCE_EXPONENTS; + case FOOBAR_MATH_TOKEN_IDENTIFIER: + case FOOBAR_MATH_TOKEN_NUMBER: + case FOOBAR_MATH_TOKEN_PAREN_OPEN: + case FOOBAR_MATH_TOKEN_PAREN_CLOSE: + default: + break; + } + } + + return INFIX_PRECEDENCE_INVALID; +} + +// +// Get the operation for an infix operator token. This requires the token to be valid. +// +FoobarMathOperation parser_operator( FoobarMathToken const* token ) +{ + switch ( token->type ) + { + case FOOBAR_MATH_TOKEN_PLUS: + return FOOBAR_MATH_OPERATION_ADD; + case FOOBAR_MATH_TOKEN_MINUS: + return FOOBAR_MATH_OPERATION_SUB; + case FOOBAR_MATH_TOKEN_MUL: + return FOOBAR_MATH_OPERATION_MUL; + case FOOBAR_MATH_TOKEN_DIV: + return FOOBAR_MATH_OPERATION_DIV; + case FOOBAR_MATH_TOKEN_POW: + return FOOBAR_MATH_OPERATION_POW; + case FOOBAR_MATH_TOKEN_IDENTIFIER: + case FOOBAR_MATH_TOKEN_NUMBER: + case FOOBAR_MATH_TOKEN_PAREN_OPEN: + case FOOBAR_MATH_TOKEN_PAREN_CLOSE: + default: + g_warn_if_reached( ); + return 0; + } +} diff --git a/csrc/services/quick-answers/math.c b/csrc/services/quick-answers/math.c new file mode 100644 index 0000000..69219e2 --- /dev/null +++ b/csrc/services/quick-answers/math.c @@ -0,0 +1,851 @@ +#define _GNU_SOURCE + +#include "services/quick-answers/math.h" +#include + +// +// FoobarMathExpression: +// +// A fully parsed mathematical expression which can be evaluated, for example "sin(pi) - 3" would be represented as: +// - Operation: SUB +// - Function: SIN +// - Constant: PI +// - Value: 3 +// + +struct _FoobarMathExpression +{ + FoobarMathExpressionType type; + + union + { + struct + { + FoobarMathValue v; + } value; + + struct + { + FoobarMathFunction f; + FoobarMathExpression* input; + } function; + + struct + { + FoobarMathConstant c; + } constant; + + struct + { + FoobarMathOperation o; + FoobarMathExpression* lhs; + FoobarMathExpression* rhs; + } operation; + }; +}; + +// +// FoobarMathValue: +// +// Representation of a value. The goal of this memory structure is to store the value as an integer for as long as +// possible. Because of this, there is a "decimal_places" field, so the actual value of an integer is +// $v * 10^(-decimal_places)$. +// +// If absolutely necessary (for example, because we use a function like sin), the value is converted to a floating-point +// value. +// +// Note that we use GMP to store the integer value to allow for arbitrarily large values. Because of this, the structure +// should always be freed using foobar_math_value_free. +// + +// --------------------------------------------------------------------------------------------------------------------- +// Expressions +// --------------------------------------------------------------------------------------------------------------------- + +// +// Helper to allocate a new FoobarMathExpression representing a value. +// +// Ownership of "value" is transferred to the new expression. +// +FoobarMathExpression* foobar_math_expression_new_value( FoobarMathValue value ) +{ + FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); + res->type = FOOBAR_MATH_EXPRESSION_VALUE; + res->value.v = value; + return res; +} + +// +// Helper to allocate a new FoobarMathExpression representing a function to be evaluated with another expression as its +// parameter. +// +// Ownership of "input" is transferred to the new expression. +// +FoobarMathExpression* foobar_math_expression_new_function( + FoobarMathFunction function, + FoobarMathExpression* input ) +{ + FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); + res->type = FOOBAR_MATH_EXPRESSION_FUNCTION; + res->function.f = function; + res->function.input = input; + return res; +} + +// +// Helper to allocate a new FoobarMathExpression representing a known constant. +// +FoobarMathExpression* foobar_math_expression_new_constant( FoobarMathConstant constant ) +{ + FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); + res->type = FOOBAR_MATH_EXPRESSION_CONSTANT; + res->constant.c = constant; + return res; +} + +// +// Helper to allocate a new FoobarMathExpression representing an operation to be evaluated with two expressions as its +// parameters. +// +// Ownership of "lhs" and "rhs" is transferred to the new expression. +// +FoobarMathExpression* foobar_math_expression_new_operation( + FoobarMathOperation operation, + FoobarMathExpression* lhs, + FoobarMathExpression* rhs ) +{ + FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); + res->type = FOOBAR_MATH_EXPRESSION_OPERATION; + res->operation.o = operation; + res->operation.lhs = lhs; + res->operation.rhs = rhs; + return res; +} + +// +// Release the memory associated with a mathematical expression, along with the expressions it owns. +// +void foobar_math_expression_free( FoobarMathExpression* expression ) +{ + if ( expression ) + { + switch ( expression->type ) + { + case FOOBAR_MATH_EXPRESSION_VALUE: + foobar_math_value_free( expression->value.v ); + break; + case FOOBAR_MATH_EXPRESSION_FUNCTION: + foobar_math_expression_free( expression->function.input ); + break; + case FOOBAR_MATH_EXPRESSION_CONSTANT: + break; + case FOOBAR_MATH_EXPRESSION_OPERATION: + foobar_math_expression_free( expression->operation.lhs ); + foobar_math_expression_free( expression->operation.rhs ); + break; + default: + g_warn_if_reached( ); + break; + } + + g_free( expression ); + } +} + +// +// Print a tree representation of a mathematical expression. +// +// This is mainly useful for debugging. "indentation" describes the current indentation level for the recursive call +// (should be 0 initially). +// +void foobar_math_expression_print( + FoobarMathExpression const* expr, + gint indentation ) +{ + for (gint i = 0; i < indentation; ++i) { g_print("| "); } + switch ( expr->type ) + { + case FOOBAR_MATH_EXPRESSION_VALUE: + { + g_autofree gchar* val = foobar_math_value_to_string( expr->value.v ); + g_print( "%s\n", val ); + break; + } + case FOOBAR_MATH_EXPRESSION_FUNCTION: + switch ( expr->function.f ) + { + case FOOBAR_MATH_FUNCTION_NEGATE: + g_print( "-\n" ); + break; + case FOOBAR_MATH_FUNCTION_SIN: + g_print( "sin\n" ); + break; + case FOOBAR_MATH_FUNCTION_COS: + g_print( "cos\n" ); + break; + case FOOBAR_MATH_FUNCTION_SEC: + g_print( "sec\n" ); + break; + case FOOBAR_MATH_FUNCTION_CSC: + g_print( "csc\n" ); + break; + case FOOBAR_MATH_FUNCTION_TAN: + g_print( "tan\n" ); + break; + case FOOBAR_MATH_FUNCTION_COT: + g_print( "cot\n" ); + break; + case FOOBAR_MATH_FUNCTION_ARCSIN: + g_print( "arcsin\n" ); + break; + case FOOBAR_MATH_FUNCTION_ARCCOS: + g_print( "arccos\n" ); + break; + case FOOBAR_MATH_FUNCTION_ARCSEC: + g_print( "arcsec\n" ); + break; + case FOOBAR_MATH_FUNCTION_ARCCSC: + g_print( "arccsc\n" ); + break; + case FOOBAR_MATH_FUNCTION_ARCTAN: + g_print( "arctan\n" ); + break; + case FOOBAR_MATH_FUNCTION_ARCCOT: + g_print( "arccot\n" ); + break; + case FOOBAR_MATH_FUNCTION_EXP: + g_print( "exp\n" ); + break; + case FOOBAR_MATH_FUNCTION_SQRT: + g_print( "sqrt\n" ); + break; + default: + g_warn_if_reached( ); + break; + } + foobar_math_expression_print( expr->function.input, indentation + 1 ); + break; + case FOOBAR_MATH_EXPRESSION_CONSTANT: + switch ( expr->constant.c ) + { + case FOOBAR_MATH_CONSTANT_PI: + g_print( "pi\n" ); + break; + case FOOBAR_MATH_CONSTANT_E: + g_print( "e\n" ); + break; + default: + g_warn_if_reached( ); + break; + } + break; + case FOOBAR_MATH_EXPRESSION_OPERATION: + switch ( expr->operation.o ) + { + case FOOBAR_MATH_OPERATION_ADD: + g_print( "+\n" ); + break; + case FOOBAR_MATH_OPERATION_SUB: + g_print( "-\n" ); + break; + case FOOBAR_MATH_OPERATION_MUL: + g_print( "*\n" ); + break; + case FOOBAR_MATH_OPERATION_DIV: + g_print( "/\n" ); + break; + case FOOBAR_MATH_OPERATION_POW: + g_print( "^\n" ); + break; + default: + g_warn_if_reached( ); + break; + } + foobar_math_expression_print( expr->operation.lhs, indentation + 1 ); + foobar_math_expression_print( expr->operation.rhs, indentation + 1 ); + break; + default: + g_warn_if_reached( ); + break; + } +} + +// +// Evaluate an expression, producing a value. +// +// On success (indicated by the return value TRUE), the value should be freed using foobar_math_value_free. +// +gboolean foobar_math_evaluate( + FoobarMathExpression const* expr, + FoobarMathValue* out_value ) +{ + switch ( expr->type ) + { + case FOOBAR_MATH_EXPRESSION_VALUE: + foobar_math_value_copy( expr->value.v, out_value ); + return TRUE; + case FOOBAR_MATH_EXPRESSION_CONSTANT: + switch ( expr->constant.c ) + { + case FOOBAR_MATH_CONSTANT_PI: + foobar_math_value_from_float( M_PIl, out_value ); + return TRUE; + case FOOBAR_MATH_CONSTANT_E: + foobar_math_value_from_float( M_El, out_value ); + return TRUE; + default: + g_warn_if_reached( ); + return FALSE; + } + case FOOBAR_MATH_EXPRESSION_FUNCTION: + { + FoobarMathValue input = { 0 }; + if ( !foobar_math_evaluate( expr->function.input, &input ) ) { return FALSE; } + + if ( expr->function.f == FOOBAR_MATH_FUNCTION_NEGATE ) + { + *out_value = input; + foobar_math_value_negate( out_value ); + return TRUE; + } + + long double input_float = foobar_math_value_to_float( input ); + foobar_math_value_free( input ); + + long double result = NAN; + switch ( expr->function.f ) + { + case FOOBAR_MATH_FUNCTION_SIN: + result = sinl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_COS: + result = cosl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_SEC: + result = 1.L / cosl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_CSC: + result = 1.L / sinl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_TAN: + result = tanl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_COT: + result = 1.L / tanl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCSIN: + result = asinl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCCOS: + result = acosl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCSEC: + result = acosl( 1.L / input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCCSC: + result = asinl( 1.L / input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCTAN: + result = atanl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_ARCCOT: + result = atanl( 1.L / input_float ); + break; + case FOOBAR_MATH_FUNCTION_EXP: + result = expl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_SQRT: + result = sqrtl( input_float ); + break; + case FOOBAR_MATH_FUNCTION_NEGATE: + default: + g_warn_if_reached( ); + return FALSE; + } + + if ( isnanl( result ) || isinfl( result ) ) { return FALSE; } + + foobar_math_value_from_float( result, out_value ); + return TRUE; + } + case FOOBAR_MATH_EXPRESSION_OPERATION: + { + FoobarMathValue lhs = { 0 }; + if ( !foobar_math_evaluate( expr->operation.lhs, &lhs ) ) { return FALSE; } + FoobarMathValue rhs = { 0 }; + if ( !foobar_math_evaluate( expr->operation.rhs, &rhs ) ) + { + foobar_math_value_free( lhs ); + return FALSE; + } + + gboolean success = FALSE; + switch ( expr->operation.o ) + { + case FOOBAR_MATH_OPERATION_ADD: + success = foobar_math_value_add( &lhs, &rhs, out_value ); + break; + case FOOBAR_MATH_OPERATION_SUB: + success = foobar_math_value_sub( &lhs, &rhs, out_value ); + break; + case FOOBAR_MATH_OPERATION_MUL: + success = foobar_math_value_mul( &lhs, &rhs, out_value ); + break; + case FOOBAR_MATH_OPERATION_DIV: + success = foobar_math_value_div( &lhs, &rhs, out_value ); + break; + case FOOBAR_MATH_OPERATION_POW: + success = foobar_math_value_pow( &lhs, &rhs, out_value ); + break; + default: + g_warn_if_reached( ); + break; + } + + foobar_math_value_free( lhs ); + foobar_math_value_free( rhs ); + + return success; + } + default: + g_warn_if_reached( ); + return FALSE; + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Values +// --------------------------------------------------------------------------------------------------------------------- + +// +// Initialize a new integer value to zero. +// +// After this, the structure should be freed using foobar_math_value_free. +// +void foobar_math_value_new_int( FoobarMathValue* out_value ) +{ + out_value->type = FOOBAR_MATH_VALUE_INT; + mpz_init( out_value->int_value.v ); + out_value->int_value.decimal_places = 0; +} + +// +// Initialize a new value from its floating point representation. +// +// Although technically not necessary, you should still call foobar_math_value_free on the returned value. +// +void foobar_math_value_from_float( + long double value, + FoobarMathValue* out_value ) +{ + out_value->type = FOOBAR_MATH_VALUE_FLOAT; + out_value->float_value.v = value; +} + +// +// Parse a value from its string representation. This will always return an integer representation. +// +// On success (return value TRUE), the structure should be freed using foobar_math_value_free. +// +gboolean foobar_math_value_from_string( + gchar const* input, + gsize input_length, + FoobarMathValue* out_value ) +{ + g_autofree gchar* without_sep = g_new0( gchar, input_length + 1 ); + gsize sep_pos = input_length - 1; + gsize j = 0; + for ( gsize i = 0; i < input_length; ++i ) + { + if ( input[i] == '.' ) + { + sep_pos = i; + } + else + { + without_sep[j++] = input[i]; + } + } + + foobar_math_value_new_int( out_value ); + out_value->int_value.decimal_places = input_length - 1 - sep_pos; + if ( mpz_init_set_str( out_value->int_value.v, without_sep, 10 ) == -1 ) + { + mpz_clear( out_value->int_value.v ); + return FALSE; + } + + return TRUE; +} + +// +// Create a copy of a value, allocating necessary memory. +// +// After this, the structure should be freed using foobar_math_value_free. +// +void foobar_math_value_copy( + FoobarMathValue value, + FoobarMathValue* out_value ) +{ + *out_value = value; + + switch ( value.type ) + { + case FOOBAR_MATH_VALUE_INT: + mpz_init_set( out_value->int_value.v, value.int_value.v ); + break; + case FOOBAR_MATH_VALUE_FLOAT: + break; + default: + g_warn_if_reached( ); + break; + } +} + +// +// Release memory associated with a value. +// +// This is necessary because the integer representation is stored using GMP, which can store arbitrarily large values. +// +void foobar_math_value_free( FoobarMathValue value ) +{ + switch ( value.type ) + { + case FOOBAR_MATH_VALUE_INT: + mpz_clear( value.int_value.v ); + break; + case FOOBAR_MATH_VALUE_FLOAT: + break; + default: + g_warn_if_reached( ); + break; + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Operations +// --------------------------------------------------------------------------------------------------------------------- + +// +// Operation equivalent to "value *= -1". +// +void foobar_math_value_negate( FoobarMathValue* value ) +{ + switch ( value->type ) + { + case FOOBAR_MATH_VALUE_INT: + mpz_neg( value->int_value.v, value->int_value.v ); + break; + case FOOBAR_MATH_VALUE_FLOAT: + value->float_value.v = -value->float_value.v; + break; + default: + g_warn_if_reached( ); + break; + } +} + +// +// Operation setting "out_value = lhs + rhs". +// +// After this, the result value should be freed using foobar_math_value_free. +// +gboolean foobar_math_value_add( + FoobarMathValue* lhs, + FoobarMathValue* rhs, + FoobarMathValue* out_value ) +{ + if ( foobar_math_value_unify_integers( lhs, rhs ) ) + { + foobar_math_value_new_int( out_value ); + out_value->int_value.decimal_places = lhs->int_value.decimal_places; + mpz_add( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v ); + return TRUE; + } + + long double lhs_float = foobar_math_value_to_float( *lhs ); + long double rhs_float = foobar_math_value_to_float( *rhs ); + foobar_math_value_from_float( lhs_float + rhs_float, out_value ); + return TRUE; +} + +// +// Operation setting "out_value = lhs - rhs". +// +// After this, the result value should be freed using foobar_math_value_free. +// +gboolean foobar_math_value_sub( + FoobarMathValue* lhs, + FoobarMathValue* rhs, + FoobarMathValue* out_value ) +{ + if ( foobar_math_value_unify_integers( lhs, rhs ) ) + { + foobar_math_value_new_int( out_value ); + out_value->int_value.decimal_places = lhs->int_value.decimal_places; + mpz_sub( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v ); + return TRUE; + } + + long double lhs_float = foobar_math_value_to_float( *lhs ); + long double rhs_float = foobar_math_value_to_float( *rhs ); + foobar_math_value_from_float( lhs_float - rhs_float, out_value ); + return TRUE; +} + +// +// Operation setting "out_value = lhs * rhs". +// +// After this, the result value should be freed using foobar_math_value_free. +// +gboolean foobar_math_value_mul( + FoobarMathValue* lhs, + FoobarMathValue* rhs, + FoobarMathValue* out_value ) +{ + if ( lhs->type == FOOBAR_MATH_VALUE_INT && rhs->type == FOOBAR_MATH_VALUE_INT ) + { + foobar_math_value_new_int( out_value ); + out_value->int_value.decimal_places = lhs->int_value.decimal_places + rhs->int_value.decimal_places; + mpz_mul( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v ); + return TRUE; + } + + long double lhs_float = foobar_math_value_to_float( *lhs ); + long double rhs_float = foobar_math_value_to_float( *rhs ); + foobar_math_value_from_float( lhs_float * rhs_float, out_value ); + return TRUE; +} + +// +// Operation setting "out_value = lhs / rhs". +// +// After this, the result value should be freed using foobar_math_value_free. +// +gboolean foobar_math_value_div( + FoobarMathValue* lhs, + FoobarMathValue* rhs, + FoobarMathValue* out_value ) +{ + if ( lhs->type == FOOBAR_MATH_VALUE_INT && rhs->type == FOOBAR_MATH_VALUE_INT ) + { + if ( mpz_sgn( rhs->int_value.v ) == 0 ) { return FALSE; } + + if ( rhs->int_value.decimal_places > lhs->int_value.decimal_places ) + { + foobar_math_value_unify_integers( lhs, rhs ); + } + + foobar_math_value_new_int( out_value ); + out_value->int_value.decimal_places = lhs->int_value.decimal_places - rhs->int_value.decimal_places; + mpz_t rem; + mpz_init( rem ); + mpz_divmod( out_value->int_value.v, rem, lhs->int_value.v, rhs->int_value.v ); + gboolean result_is_int = mpz_sgn( rem ) == 0; + mpz_clear( rem ); + + if ( result_is_int ) { return TRUE; } + foobar_math_value_free( *out_value ); + } + + long double lhs_float = foobar_math_value_to_float( *lhs ); + long double rhs_float = foobar_math_value_to_float( *rhs ); + long double result = lhs_float / rhs_float; + if ( isnanl( result ) || isinfl( result ) ) { return FALSE; } + foobar_math_value_from_float( result, out_value ); + return TRUE; +} + +// +// Operation setting "out_value = lhs ^ rhs". +// +// After this, the result value should be freed using foobar_math_value_free. +// +gboolean foobar_math_value_pow( + FoobarMathValue* lhs, + FoobarMathValue* rhs, + FoobarMathValue* out_value ) +{ + if ( lhs->type == FOOBAR_MATH_VALUE_INT && + rhs->type == FOOBAR_MATH_VALUE_INT && + rhs->int_value.decimal_places == 0 && + mpz_fits_ulong_p( rhs->int_value.v ) ) + { + unsigned long exp = mpz_get_ui( rhs->int_value.v ); + foobar_math_value_new_int( out_value ); + out_value->int_value.decimal_places = lhs->int_value.decimal_places * exp; + mpz_pow_ui( out_value->int_value.v, lhs->int_value.v, exp ); + return TRUE; + } + + long double lhs_float = foobar_math_value_to_float( *lhs ); + long double rhs_float = foobar_math_value_to_float( *rhs ); + long double result = powl( lhs_float, rhs_float ); + if ( isnanl( result ) || isinfl( result ) ) { return FALSE; } + foobar_math_value_from_float( result, out_value ); + return TRUE; +} + +// +// Ensure that two integer values have the same number of decimal places. +// +// This may change the in-memory-representation of a value, but not its actual value. It only increases the precision of +// one value +// +// If either a or b is not stored as an integer, this will return FALSE. +// +gboolean foobar_math_value_unify_integers( + FoobarMathValue* a, + FoobarMathValue* b ) +{ + if ( a->type != FOOBAR_MATH_VALUE_INT || b->type != FOOBAR_MATH_VALUE_INT ) { return FALSE; } + + // Extend the less precise representation so both numbers have the same number of decimal places. + + gsize decimal_places = MAX( a->int_value.decimal_places, b->int_value.decimal_places ); + + mpz_t exp; + mpz_init( exp ); + + if ( decimal_places > a->int_value.decimal_places ) + { + mpz_ui_pow_ui( exp, 10, decimal_places - a->int_value.decimal_places ); + mpz_mul( a->int_value.v, a->int_value.v, exp ); + } + + if ( decimal_places > b->int_value.decimal_places ) + { + mpz_ui_pow_ui( exp, 10, decimal_places - b->int_value.decimal_places ); + mpz_mul( b->int_value.v, b->int_value.v, exp ); + } + + mpz_clear( exp ); + + a->int_value.decimal_places = b->int_value.decimal_places = decimal_places; + + return TRUE; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Conversions +// --------------------------------------------------------------------------------------------------------------------- + +// +// Format a value as a string. +// +// The result is a null-terminated string which should be freed using g_free. +// +gchar* foobar_math_value_to_string( FoobarMathValue value ) +{ + switch ( value.type ) + { + case FOOBAR_MATH_VALUE_INT: + { + gsize cap = mpz_sizeinbase( value.int_value.v, 10 ); + cap = MAX( cap, value.int_value.decimal_places + 1 ); // pad with zeroes if necessary + gchar* result = g_new0( gchar, cap + 3 ); // length + null terminator + sign + separator + mpz_get_str( result, 10, value.int_value.v ); + gsize len = strlen( result ); // actual size may be 1 char less (+ len now also includes sign) + gboolean has_sign = result[0] == '-'; + if ( has_sign ) + { + // add the sign back later, makes everything else easier + result += 1; + len -= 1; + } + // pad with leading zeroes if necessary + gsize padded_len = MAX( len, value.int_value.decimal_places + 1 ); + memmove( &result[padded_len - len], result, len + 1 ); + memset( result, '0', padded_len - len ); + len = padded_len; + gboolean trailing_zeros = TRUE; + gsize decimal_places = value.int_value.decimal_places; + gsize actual_decimal_places = decimal_places; + for ( gsize i = 0; i < decimal_places; ++i ) + { + if ( trailing_zeros && result[len - 1 - i] == '0' ) + { + result[len - 1 - i] = '\0'; + actual_decimal_places -= 1; + } + else + { + trailing_zeros = FALSE; + } + } + if ( actual_decimal_places > 0 ) + { + memmove( &result[len + 1 - decimal_places], &result[len - decimal_places], decimal_places + 1 ); + result[len - decimal_places] = '.'; + } + if ( has_sign ) + { + result -= 1; + len += 1; + } + return result; + } + case FOOBAR_MATH_VALUE_FLOAT: + { + gchar* result = g_strdup_printf( "%.30Lf", value.float_value.v ); + + // trim trailing zeros + size_t len = strlen( result ); + for ( gsize i = 0; i < len; ++i ) + { + if ( result[len - 1 - i] == '.' ) + { + result[len - 1 - i] = '\0'; + break; + } + else if ( result[len - 1 - i] == '0' ) + { + result[len - 1 - i] = '\0'; + } + else + { + break; + } + } + + return result; + } + default: + g_warn_if_reached( ); + return NULL; + } +} + +// +// Convert a value to its floating-point representation. +// +// This may be a lossy conversion and should only be done if absolutely necessary. +// +long double foobar_math_value_to_float( FoobarMathValue value ) +{ + switch ( value.type ) + { + case FOOBAR_MATH_VALUE_INT: + { + mpz_t exp, integer, frac; + mpz_inits( exp, integer, frac, NULL ); + mpz_ui_pow_ui( exp, 10, value.int_value.decimal_places ); + mpz_mod( frac, value.int_value.v, exp ); + mpz_div( integer, value.int_value.v, exp ); + mpf_t f_exp, f_frac; + mpf_inits( f_exp, f_frac, NULL ); + mpf_set_z( f_exp, exp ); + mpf_set_z( f_frac, frac ); + mpf_div( f_frac, f_frac, f_exp ); + long double result = mpz_get_d( integer ) + mpf_get_d( f_frac ); + mpf_clears( f_exp, f_frac, NULL ); + mpz_clears( exp, integer, frac, NULL ); + return result; + } + case FOOBAR_MATH_VALUE_FLOAT: + return value.float_value.v; + default: + g_warn_if_reached( ); + return 0; + } +} diff --git a/csrc/services/quick-answers/math.h b/csrc/services/quick-answers/math.h new file mode 100644 index 0000000..fafa73d --- /dev/null +++ b/csrc/services/quick-answers/math.h @@ -0,0 +1,148 @@ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +typedef enum +{ + FOOBAR_MATH_VALUE_INT, + FOOBAR_MATH_VALUE_FLOAT, +} FoobarMathValueType; + +typedef struct _FoobarMathValue FoobarMathValue; + +struct _FoobarMathValue +{ + FoobarMathValueType type; + + union + { + struct + { + mpz_t v; + gsize decimal_places; + } int_value; + + struct + { + long double v; + } float_value; + }; +}; + +typedef enum +{ + FOOBAR_MATH_TOKEN_IDENTIFIER, + FOOBAR_MATH_TOKEN_NUMBER, + FOOBAR_MATH_TOKEN_PAREN_OPEN, + FOOBAR_MATH_TOKEN_PAREN_CLOSE, + FOOBAR_MATH_TOKEN_PLUS, + FOOBAR_MATH_TOKEN_MINUS, + FOOBAR_MATH_TOKEN_MUL, + FOOBAR_MATH_TOKEN_DIV, + FOOBAR_MATH_TOKEN_POW, +} FoobarMathTokenType; + +typedef struct _FoobarMathToken FoobarMathToken; + +struct _FoobarMathToken +{ + FoobarMathTokenType type; + gchar const* data; + gsize length; +}; + +typedef enum +{ + FOOBAR_MATH_EXPRESSION_VALUE, + FOOBAR_MATH_EXPRESSION_FUNCTION, + FOOBAR_MATH_EXPRESSION_CONSTANT, + FOOBAR_MATH_EXPRESSION_OPERATION, +} FoobarMathExpressionType; + +typedef enum +{ + FOOBAR_MATH_FUNCTION_NEGATE, + FOOBAR_MATH_FUNCTION_SIN, + FOOBAR_MATH_FUNCTION_COS, + FOOBAR_MATH_FUNCTION_SEC, + FOOBAR_MATH_FUNCTION_CSC, + FOOBAR_MATH_FUNCTION_TAN, + FOOBAR_MATH_FUNCTION_COT, + FOOBAR_MATH_FUNCTION_ARCSIN, + FOOBAR_MATH_FUNCTION_ARCCOS, + FOOBAR_MATH_FUNCTION_ARCSEC, + FOOBAR_MATH_FUNCTION_ARCCSC, + FOOBAR_MATH_FUNCTION_ARCTAN, + FOOBAR_MATH_FUNCTION_ARCCOT, + FOOBAR_MATH_FUNCTION_EXP, + FOOBAR_MATH_FUNCTION_SQRT, +} FoobarMathFunction; + +typedef enum +{ + FOOBAR_MATH_CONSTANT_PI, + FOOBAR_MATH_CONSTANT_E, +} FoobarMathConstant; + +typedef enum +{ + FOOBAR_MATH_OPERATION_ADD, + FOOBAR_MATH_OPERATION_SUB, + FOOBAR_MATH_OPERATION_MUL, + FOOBAR_MATH_OPERATION_DIV, + FOOBAR_MATH_OPERATION_POW, +} FoobarMathOperation; + +typedef struct _FoobarMathExpression FoobarMathExpression; + +FoobarMathToken* foobar_math_lex ( gchar const* input, + gsize input_length, + gsize* out_count ); +FoobarMathExpression* foobar_math_parse ( FoobarMathToken const* tokens, + gsize tokens_count ); +FoobarMathExpression* foobar_math_expression_new_value ( FoobarMathValue value ); +FoobarMathExpression* foobar_math_expression_new_function ( FoobarMathFunction function, + FoobarMathExpression* input ); +FoobarMathExpression* foobar_math_expression_new_constant ( FoobarMathConstant constant ); +FoobarMathExpression* foobar_math_expression_new_operation( FoobarMathOperation operation, + FoobarMathExpression* lhs, + FoobarMathExpression* rhs ); +void foobar_math_expression_print ( FoobarMathExpression const* expr, + gint indentation ); +gboolean foobar_math_evaluate ( FoobarMathExpression const* expr, + FoobarMathValue* out_value ); +void foobar_math_expression_free ( FoobarMathExpression* expression ); +void foobar_math_value_new_int ( FoobarMathValue* out_value ); +void foobar_math_value_from_float ( long double value, + FoobarMathValue* out_value ); +gboolean foobar_math_value_from_string ( gchar const* input, + gsize input_length, + FoobarMathValue* out_value ); +void foobar_math_value_copy ( FoobarMathValue value, + FoobarMathValue* out_value ); +void foobar_math_value_free ( FoobarMathValue value ); +void foobar_math_value_negate ( FoobarMathValue* value ); +gboolean foobar_math_value_add ( FoobarMathValue* lhs, + FoobarMathValue* rhs, + FoobarMathValue* out_value ); +gboolean foobar_math_value_sub ( FoobarMathValue* lhs, + FoobarMathValue* rhs, + FoobarMathValue* out_value ); +gboolean foobar_math_value_mul ( FoobarMathValue* lhs, + FoobarMathValue* rhs, + FoobarMathValue* out_value ); +gboolean foobar_math_value_div ( FoobarMathValue* lhs, + FoobarMathValue* rhs, + FoobarMathValue* out_value ); +gboolean foobar_math_value_pow ( FoobarMathValue* lhs, + FoobarMathValue* rhs, + FoobarMathValue* out_value ); +gboolean foobar_math_value_unify_integers ( FoobarMathValue* a, + FoobarMathValue* b ); +gchar* foobar_math_value_to_string ( FoobarMathValue value ); +long double foobar_math_value_to_float ( FoobarMathValue value ); + +G_END_DECLS diff --git a/csrc/services/quick-answers/math.test.c b/csrc/services/quick-answers/math.test.c new file mode 100644 index 0000000..416b576 --- /dev/null +++ b/csrc/services/quick-answers/math.test.c @@ -0,0 +1,153 @@ +#include "services/quick-answers/math.h" +#include + +#define SERIALIZATION_TEST( value, identifier ) \ + static void serialization_value_##identifier##_spec( void ) \ + { \ + FoobarMathValue val; \ + \ + gchar const input[] = #value; \ + mutest_expect( \ + "parse result", \ + mutest_bool_value( foobar_math_value_from_string( input, sizeof(input) - 1, &val) ), \ + mutest_to_be_true, \ + NULL ); \ + g_autofree gchar* output = foobar_math_value_to_string( val ); \ + mutest_expect( \ + "string representation", \ + mutest_string_value( output ), \ + mutest_to_be, \ + input, \ + NULL ); \ + \ + foobar_math_value_free( val ); \ + } + +SERIALIZATION_TEST( 0, 0 ) +SERIALIZATION_TEST( 1, 1 ) +SERIALIZATION_TEST( -1, neg_1 ) +SERIALIZATION_TEST( 1.2, 1_2 ) +SERIALIZATION_TEST( -1.2, neg_1_2 ) +SERIALIZATION_TEST( 1.30002, 1_30002 ) +SERIALIZATION_TEST( -1.30002, neg_1_30002 ) +SERIALIZATION_TEST( 0.2, 0_2 ) +SERIALIZATION_TEST( -0.2, neg_0_2 ) +SERIALIZATION_TEST( 0.00002, 0_00002 ) +SERIALIZATION_TEST( -0.00002, neg_0_00002 ) +SERIALIZATION_TEST( 123, 123 ) +SERIALIZATION_TEST( 123.456, 123_456 ) +SERIALIZATION_TEST( -123, neg_123 ) +SERIALIZATION_TEST( -123.456, neg_123_456 ) + +#undef SERIALIZATION_TEST + +static void serialization_suite( void ) +{ + mutest_it( "value 0", serialization_value_0_spec ); + mutest_it( "value 1", serialization_value_1_spec ); + mutest_it( "value -1", serialization_value_neg_1_spec ); + mutest_it( "value 1.2", serialization_value_1_2_spec ); + mutest_it( "value -1.2", serialization_value_neg_1_2_spec ); + mutest_it( "value 1.30002", serialization_value_1_30002_spec ); + mutest_it( "value -1.30002", serialization_value_neg_1_30002_spec ); + mutest_it( "value 0.2", serialization_value_0_2_spec ); + mutest_it( "value -0.2", serialization_value_neg_0_2_spec ); + mutest_it( "value 0.00002", serialization_value_0_00002_spec ); + mutest_it( "value -0.00002", serialization_value_neg_0_00002_spec ); + mutest_it( "value 123", serialization_value_123_spec ); + mutest_it( "value 123.456", serialization_value_123_456_spec ); + mutest_it( "value -123", serialization_value_neg_123_spec ); + mutest_it( "value -123.456", serialization_value_neg_123_456_spec ); +} +#define OPERATION_TEST( op, value_a, value_b, value_res, identifier ) \ + static void operation_##identifier##_spec( void ) \ + { \ + FoobarMathValue a, b, res; \ + \ + gchar const input_a[] = #value_a; \ + gchar const input_b[] = #value_b; \ + mutest_expect( \ + "input a parsed", \ + mutest_bool_value( foobar_math_value_from_string( input_a, sizeof(input_a) - 1, &a) ), \ + mutest_to_be_true, \ + NULL ); \ + mutest_expect( \ + "input b parsed", \ + mutest_bool_value( foobar_math_value_from_string( input_b, sizeof(input_b) - 1, &b) ), \ + mutest_to_be_true, \ + NULL ); \ + foobar_math_value_##op( &a, &b, &res ); \ + g_autofree gchar* output = foobar_math_value_to_string( res ); \ + mutest_expect( \ + "result", \ + mutest_string_value( output ), \ + mutest_to_be, \ + #value_res, \ + NULL ); \ + \ + foobar_math_value_free( a ); \ + foobar_math_value_free( b ); \ + foobar_math_value_free( res ); \ + } + +OPERATION_TEST( add, 123, 456, 579, 123_plus_456) +OPERATION_TEST( add, 123.456, 11, 134.456, 123_456_plus_11) +OPERATION_TEST( add, 11, 123.456, 134.456, 11_plus_123_456) +OPERATION_TEST( sub, 123, 456, -333, 123_minus_456) +OPERATION_TEST( sub, 123.456, 11, 112.456, 123_456_minus_11) +OPERATION_TEST( sub, 11, 123.456, -112.456, 11_minus_123_456) +OPERATION_TEST( mul, 123, 3, 369, 123_times_3 ) +OPERATION_TEST( mul, 12, 0.05, 0.6, 12_times_0_05 ) +OPERATION_TEST( mul, 0.05, 12, 0.6, 0_05_times_12 ) +OPERATION_TEST( div, 369, 3, 123, 369_div_3 ) +OPERATION_TEST( div, 369, 2, 184.5, 369_div_2 ) +OPERATION_TEST( div, 12, 0.05, 240, 12_div_0_05 ) +OPERATION_TEST( div, 0.08, 4, 0.02, 0_08_div_4 ) +OPERATION_TEST( pow, 2, 3, 8, 2_pow_3 ) +OPERATION_TEST( pow, 4, 0.5, 2, 4_pow_0_5 ) + +#undef OPERATION_TEST + +static void addition_suite( void ) +{ + mutest_it( "calculate 123 + 456", operation_123_plus_456_spec ); + mutest_it( "calculate 123.456 + 11", operation_123_456_plus_11_spec ); + mutest_it( "calculate 11 + 123.456", operation_11_plus_123_456_spec ); +} + +static void subtraction_suite( void ) +{ + mutest_it( "calculate 123 - 456", operation_123_minus_456_spec ); + mutest_it( "calculate 123.456 - 11", operation_123_456_minus_11_spec ); + mutest_it( "calculate 11 - 123.456", operation_11_minus_123_456_spec ); +} + +static void multiplication_suite( void ) +{ + mutest_it( "calculate 123 * 3", operation_123_times_3_spec ); + mutest_it( "calculate 12 * 0.05", operation_12_times_0_05_spec ); + mutest_it( "calculate 0.05 * 12", operation_0_05_times_12_spec ); +} + +static void division_suite( void ) +{ + mutest_it( "calculate 369 / 3", operation_369_div_3_spec ); + mutest_it( "calculate 369 / 2", operation_369_div_2_spec ); + mutest_it( "calculate 12 / 0.05", operation_12_div_0_05_spec ); + mutest_it( "calculate 0.08 / 4", operation_0_08_div_4_spec ); +} + +static void power_suite( void ) +{ + mutest_it( "calculate 2 ^ 3", operation_2_pow_3_spec ); + mutest_it( "calculate 4 ^ 0.5", operation_4_pow_0_5_spec ); +} + +MUTEST_MAIN( + mutest_describe( "Serialization", serialization_suite ); + mutest_describe( "Addition", addition_suite ); + mutest_describe( "Subtraction", subtraction_suite ); + mutest_describe( "Multiplication", multiplication_suite ); + mutest_describe( "Division", division_suite ); + mutest_describe( "Power", power_suite ); +) diff --git a/csrc/services/quick-answers/meson.build b/csrc/services/quick-answers/meson.build new file mode 100644 index 0000000..ae468c2 --- /dev/null +++ b/csrc/services/quick-answers/meson.build @@ -0,0 +1,9 @@ +foobar_sources += files( + 'math-lexer.c', + 'math-parser.c', + 'math.c', +) + +foobar_tests += { + 'math': files('math.test.c'), +} diff --git a/csrc/services/workspace-service.c b/csrc/services/workspace-service.c new file mode 100644 index 0000000..75ea06a --- /dev/null +++ b/csrc/services/workspace-service.c @@ -0,0 +1,1386 @@ +#include "services/workspace-service.h" +#include +#include +#include + +typedef struct _EventData EventData; +typedef struct _EventHandler EventHandler; + +// +// FoobarWorkspaceFlags: +// +// Some information about the type/state of a workspace. +// + +G_DEFINE_FLAGS_TYPE( + FoobarWorkspaceFlags, + foobar_workspace_flags, + G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_NONE, "none" ), + G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_ACTIVE, "active" ), + G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_VISIBLE, "visible" ), + G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_SPECIAL, "special" ), + G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_PERSISTENT, "persistent" ), + G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_URGENT, "urgent" ) ) + +// +// FoobarWorkspace: +// +// Representation of a workspace. +// + +struct _FoobarWorkspace +{ + GObject parent_instance; + FoobarWorkspaceService* service; + gint64 id; + gchar* name; + gchar* monitor; + FoobarWorkspaceFlags flags; +}; + +enum +{ + WORKSPACE_PROP_ID = 1, + WORKSPACE_PROP_NAME, + WORKSPACE_PROP_MONITOR, + WORKSPACE_PROP_FLAGS, + N_WORKSPACE_PROPS, +}; + +static GParamSpec* workspace_props[N_WORKSPACE_PROPS] = { 0 }; + +static void foobar_workspace_class_init ( FoobarWorkspaceClass* klass ); +static void foobar_workspace_init ( FoobarWorkspace* self ); +static void foobar_workspace_get_property( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_workspace_finalize ( GObject* object ); +static FoobarWorkspace* foobar_workspace_new ( FoobarWorkspaceService* service ); +static void foobar_workspace_set_id ( FoobarWorkspace* self, + gint64 value ); +static void foobar_workspace_set_name ( FoobarWorkspace* self, + gchar const* value ); +static void foobar_workspace_set_monitor ( FoobarWorkspace* self, + gchar const* value ); +static void foobar_workspace_set_flags ( FoobarWorkspace* self, + FoobarWorkspaceFlags value ); + +G_DEFINE_FINAL_TYPE( FoobarWorkspace, foobar_workspace, G_TYPE_OBJECT ) + +// +// FoobarWorkspaceService: +// +// Service monitoring the workspaces provided by the window manager. This is implemented by communicating directly with +// Hyprland through its IPC socket. +// +// Based on the waybar implementation: +// https://github.com/Alexays/Waybar/blob/master/src/modules/hyprland/workspaces.cpp +// + +struct _FoobarWorkspaceService +{ + GObject parent_instance; + GListStore* workspaces; + GtkSortListModel* sorted_workspaces; + GCancellable* event_cancellable; + GThread* event_thread; + gchar* rx_path; + gchar* tx_path; +}; + +enum +{ + PROP_WORKSPACES = 1, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +enum +{ + SIGNAL_MONITOR_CONFIGURATION_CHANGED, + N_SIGNALS, +}; +static unsigned signals[N_SIGNALS] = { 0 }; + +static void foobar_workspace_service_class_init ( FoobarWorkspaceServiceClass* klass ); +static void foobar_workspace_service_init ( FoobarWorkspaceService* self ); +static void foobar_workspace_service_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_workspace_service_dispose ( GObject* object ); +static void foobar_workspace_service_finalize ( GObject* object ); +static gboolean foobar_workspace_service_handle_workspace_created ( EventData* data ); +static gboolean foobar_workspace_service_handle_workspace_destroyed ( EventData* data ); +static gboolean foobar_workspace_service_handle_workspace_moved ( EventData* data ); +static gboolean foobar_workspace_service_handle_workspace_renamed ( EventData* data ); +static gboolean foobar_workspace_service_handle_workspace_activated ( EventData* data ); +static gboolean foobar_workspace_service_handle_special_workspace_activated( EventData* data ); +static gboolean foobar_workspace_service_handle_monitor_focused ( EventData* data ); +static gboolean foobar_workspace_service_handle_window_urgent ( EventData* data ); +static gboolean foobar_workspace_service_handle_config_reloaded ( EventData* data ); +static gpointer foobar_workspace_service_event_thread_func ( gpointer userdata ); +static void foobar_workspace_service_dispatch_event ( FoobarWorkspaceService* self, + gchar* message ); +static gchar* foobar_workspace_service_send_request ( FoobarWorkspaceService* self, + gchar const* request, + GError** error ); +static JsonNode* foobar_workspace_service_send_json_request ( FoobarWorkspaceService* self, + gchar const* request, + GError** error ); +static void foobar_workspace_service_initialize ( FoobarWorkspaceService* self ); +static void foobar_workspace_service_update_active ( FoobarWorkspaceService* self, + GListStore* workspaces ); +static void foobar_workspace_service_add_workspace ( FoobarWorkspaceService* self, + GListStore* workspaces, + JsonObject* workspace_object, + gboolean is_persistent ); +static FoobarWorkspace* foobar_workspace_service_find_workspace ( GListModel* workspaces, + gint64 id, + guint* out_index ); +static gboolean foobar_workspace_service_is_invalid_name ( gchar const* name ); +static gint64 foobar_workspace_service_parse_id ( gchar const* str ); +static gchar* foobar_workspace_service_get_hyprland_base_path ( void ); +static gint foobar_workspace_service_compare_ids ( gconstpointer a, + gconstpointer b ); +static gint foobar_workspace_service_sort_func ( gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarWorkspaceService, foobar_workspace_service, G_TYPE_OBJECT ) + +// +// EventData: +// +// Custom userdata passed to hyprland event handlers in a single pointer. +// + +struct _EventData +{ + FoobarWorkspaceService* service; + gchar* name; + gchar* payload; +}; + +static EventData* event_data_new ( FoobarWorkspaceService* service, + gchar const* name, + gchar const* payload ); +static void event_data_free( EventData* self ); + +// +// EventHandler: +// +// Description for the handler of a single hyprland event. +// + +struct _EventHandler +{ + gchar const* name; + gboolean ( *fn )( EventData* data ); +}; + +// +// Static list of registered event handlers. +// +static EventHandler const event_handlers[] = + { + { .name = "createworkspacev2", .fn = foobar_workspace_service_handle_workspace_created }, + { .name = "destroyworkspacev2", .fn = foobar_workspace_service_handle_workspace_destroyed }, + { .name = "moveworkspacev2", .fn = foobar_workspace_service_handle_workspace_moved }, + { .name = "renameworkspace", .fn = foobar_workspace_service_handle_workspace_renamed }, + { .name = "workspace", .fn = foobar_workspace_service_handle_workspace_activated }, + { .name = "activespecial", .fn = foobar_workspace_service_handle_special_workspace_activated }, + { .name = "focusedmon", .fn = foobar_workspace_service_handle_monitor_focused }, + { .name = "urgent", .fn = foobar_workspace_service_handle_window_urgent }, + { .name = "configreloaded", .fn = foobar_workspace_service_handle_config_reloaded }, + }; + +// --------------------------------------------------------------------------------------------------------------------- +// Workspace +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for workspaces. +// +void foobar_workspace_class_init( FoobarWorkspaceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_workspace_get_property; + object_klass->finalize = foobar_workspace_finalize; + + workspace_props[WORKSPACE_PROP_ID] = g_param_spec_int64( + "id", + "ID", + "Numeric ID of the workspace.", + INT64_MIN, + INT64_MAX, + 0, + G_PARAM_READABLE ); + workspace_props[WORKSPACE_PROP_NAME] = g_param_spec_string( + "name", + "Name", + "Optional human-readable workspace name.", + NULL, + G_PARAM_READABLE ); + workspace_props[WORKSPACE_PROP_MONITOR] = g_param_spec_string( + "monitor", + "Monitor", + "Connector ID of the workspace's monitor.", + NULL, + G_PARAM_READABLE ); + workspace_props[WORKSPACE_PROP_FLAGS] = g_param_spec_flags( + "flags", + "Flags", + "Flags indicating the current state of the workspace.", + FOOBAR_TYPE_WORKSPACE_FLAGS, + FOOBAR_WORKSPACE_FLAGS_NONE, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_WORKSPACE_PROPS, workspace_props ); +} + +// +// Instance initialization for workspaces. +// +void foobar_workspace_init( FoobarWorkspace* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_workspace_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarWorkspace* self = (FoobarWorkspace*)object; + + switch ( prop_id ) + { + case WORKSPACE_PROP_ID: + g_value_set_int64( value, foobar_workspace_get_id( self ) ); + break; + case WORKSPACE_PROP_NAME: + g_value_set_string( value, foobar_workspace_get_name( self ) ); + break; + case WORKSPACE_PROP_MONITOR: + g_value_set_string( value, foobar_workspace_get_monitor( self ) ); + break; + case WORKSPACE_PROP_FLAGS: + g_value_set_flags( value, foobar_workspace_get_flags( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for workspaces. +// +void foobar_workspace_finalize( GObject* object ) +{ + FoobarWorkspace* self = (FoobarWorkspace*)object; + + g_clear_pointer( &self->name, g_free ); + g_clear_pointer( &self->monitor, g_free ); + + G_OBJECT_CLASS( foobar_workspace_parent_class )->finalize( object ); +} + +// +// Create a new workspace that is owned by the given service (captured as an unowned reference). +// +FoobarWorkspace* foobar_workspace_new( FoobarWorkspaceService* service ) +{ + FoobarWorkspace* self = g_object_new( FOOBAR_TYPE_WORKSPACE, NULL ); + self->service = service; + return self; +} + +// +// Numeric ID of the workspace. +// +gint64 foobar_workspace_get_id( FoobarWorkspace* self ) +{ + g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), 0 ); + return self->id; +} + +// +// Optional human-readable workspace name. +// +gchar const* foobar_workspace_get_name( FoobarWorkspace* self ) +{ + g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), NULL ); + return self->name; +} + +// +// Connector ID of the workspace's monitor. +// +gchar const* foobar_workspace_get_monitor( FoobarWorkspace* self ) +{ + g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), NULL ); + return self->monitor; +} + +// +// Flags indicating the current state of the workspace. +// +FoobarWorkspaceFlags foobar_workspace_get_flags( FoobarWorkspace* self ) +{ + g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), FOOBAR_WORKSPACE_FLAGS_NONE ); + return self->flags; +} + +// +// Numeric ID of the workspace. +// +void foobar_workspace_set_id( + FoobarWorkspace* self, + gint64 value ) +{ + g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); + + if ( self->id != value ) + { + self->id = value; + g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_ID] ); + } +} + +// +// Optional human-readable workspace name. +// +void foobar_workspace_set_name( + FoobarWorkspace* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); + + if ( g_strcmp0( self->name, value ) ) + { + g_clear_pointer( &self->name, g_free ); + self->name = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_NAME] ); + } +} + +// +// Connector ID of the workspace's monitor. +// +void foobar_workspace_set_monitor( + FoobarWorkspace* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); + + if ( g_strcmp0( self->monitor, value ) ) + { + g_clear_pointer( &self->monitor, g_free ); + self->monitor = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_MONITOR] ); + } +} + +// +// Flags indicating the current state of the workspace. +// +void foobar_workspace_set_flags( + FoobarWorkspace* self, + FoobarWorkspaceFlags value ) +{ + g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); + + if ( self->flags != value ) + { + self->flags = value; + g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_FLAGS] ); + } +} + +// +// Let this workspace become the active one. +// +void foobar_workspace_activate( FoobarWorkspace* self ) +{ + g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); + if ( !self->service ) { return; } + + g_autofree gchar* request = NULL; + if ( foobar_workspace_get_id( self ) > 0 ) + { + // Normal + request = g_strdup_printf( "dispatch workspace %lld", (long long)foobar_workspace_get_id( self ) ); + } + else if ( !( foobar_workspace_get_flags( self ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) ) + { + // Named (this includes persistent) + request = g_strdup_printf( "dispatch workspace name:%s", foobar_workspace_get_name( self ) ); + } + else if ( foobar_workspace_get_id( self ) != -99 ) + { + // Named special + request = g_strdup_printf( "dispatch togglespecialworkspace %s", foobar_workspace_get_name( self ) ); + } + else + { + // Special + request = g_strdup( "dispatch togglespecialworkspace" ); + } + + g_autoptr( GError ) error = NULL; + g_free( foobar_workspace_service_send_request( self->service, request, &error ) ); + if ( error ) + { + g_warning( "Unable to activate workspace: %s", error->message ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the workspace service. +// +void foobar_workspace_service_class_init( FoobarWorkspaceServiceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_workspace_service_get_property; + object_klass->dispose = foobar_workspace_service_dispose; + object_klass->finalize = foobar_workspace_service_finalize; + + props[PROP_WORKSPACES] = g_param_spec_object( + "workspaces", + "Workspaces", + "Sorted list of all workspaces.", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); + + signals[SIGNAL_MONITOR_CONFIGURATION_CHANGED] = g_signal_new( + "monitor-configuration-changed", + FOOBAR_TYPE_WORKSPACE_SERVICE, + G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0 ); +} + +// +// Instance initialization for the workspace service. +// +void foobar_workspace_service_init( FoobarWorkspaceService* self ) +{ + self->workspaces = g_list_store_new( FOOBAR_TYPE_WORKSPACE ); + + GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_workspace_service_sort_func, NULL, NULL ); + self->sorted_workspaces = gtk_sort_list_model_new( + G_LIST_MODEL( g_object_ref( self->workspaces ) ), + GTK_SORTER( sorter ) ); + + g_autofree gchar* base_path = foobar_workspace_service_get_hyprland_base_path( ); + if ( !base_path ) + { + g_warning( "Unable to connect to hyprland -- is it currently running?" ); + return; + } + + self->tx_path = g_strdup_printf( "%s/.socket.sock", base_path ); + self->rx_path = g_strdup_printf( "%s/.socket2.sock", base_path ); + self->event_cancellable = g_cancellable_new( ); + self->event_thread = g_thread_new( "workspace-event-listener", foobar_workspace_service_event_thread_func, self ); + + foobar_workspace_service_initialize( self ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_workspace_service_get_property( GObject* object, guint prop_id, GValue* value, GParamSpec* pspec ) +{ + FoobarWorkspaceService* self = (FoobarWorkspaceService*)object; + + switch ( prop_id ) + { + case PROP_WORKSPACES: + g_value_set_object( value, foobar_workspace_service_get_workspaces( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for the workspace service. +// +// This is where the event thread is joined because it might still create new object references which is a bad thing to +// do in finalize. +// +void foobar_workspace_service_dispose( GObject* object ) +{ + FoobarWorkspaceService* self = (FoobarWorkspaceService*)object; + + if ( self->event_thread ) + { + g_cancellable_cancel( self->event_cancellable ); + g_thread_join( self->event_thread ); + self->event_thread = NULL; + } + + G_OBJECT_CLASS( foobar_workspace_service_parent_class )->dispose( object ); +} + +// +// Instance cleanup for the workspace service. +// +void foobar_workspace_service_finalize( GObject* object ) +{ + FoobarWorkspaceService* self = (FoobarWorkspaceService*)object; + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->workspaces ) ); ++i ) + { + FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( self->workspaces ), i ); + workspace->service = NULL; + } + g_clear_object( &self->event_cancellable ); + g_clear_object( &self->sorted_workspaces ); + g_clear_object( &self->workspaces ); + g_clear_pointer( &self->rx_path, g_free ); + g_clear_pointer( &self->tx_path, g_free ); + + G_OBJECT_CLASS( foobar_workspace_service_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new workspace service instance. +// +FoobarWorkspaceService* foobar_workspace_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_WORKSPACE_SERVICE, NULL ); +} + +// +// Get a sorted list of all workspaces. +// +GListModel* foobar_workspace_service_get_workspaces( FoobarWorkspaceService* self ) +{ + g_return_val_if_fail( FOOBAR_IS_WORKSPACE_SERVICE( self ), NULL ); + return G_LIST_MODEL( self->sorted_workspaces ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Event Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Handler for the "createworkspacev2" event. +// +// The payload has the format "," where is the numeric ID. +// +gboolean foobar_workspace_service_handle_workspace_created( EventData* data ) +{ + gchar* save; + gchar const* id_str = strtok_r( data->payload, ",", &save ); + gchar const* name = strtok_r( NULL, ",", &save ); + g_return_val_if_fail( id_str && name, G_SOURCE_REMOVE ); + + if ( foobar_workspace_service_is_invalid_name( name ) ) { return G_SOURCE_REMOVE; } + gint64 id = foobar_workspace_service_parse_id( id_str ); + + // Get additional information about the workspace. + + g_autoptr( GError ) error = NULL; + g_autoptr( JsonNode ) workspaces_node = foobar_workspace_service_send_json_request( + data->service, + "j/workspaces", + &error ); + if ( !workspaces_node ) + { + g_warning( "Unable to load workspaces: %s", error->message ); + return G_SOURCE_REMOVE; + } + + JsonArray* workspaces_array = json_node_get_array( workspaces_node ); + JsonObject* workspace_object = NULL; + for ( guint i = 0; i < json_array_get_length( workspaces_array ); ++i ) + { + JsonObject* it = json_array_get_object_element( workspaces_array, i ); + if ( id == json_object_get_int_member( it, "id" ) ) + { + workspace_object = it; + break; + } + } + + if ( !workspace_object ) + { + g_warning( "Unable to find new workspace." ); + return G_SOURCE_REMOVE; + } + + // Check if the workspace is persistent by looking for a rule with its ID. + + gboolean is_persistent = FALSE; + + g_autoptr( JsonNode ) rules_node = foobar_workspace_service_send_json_request( + data->service, + "j/workspacerules", + &error ); + if ( !rules_node ) + { + g_warning( "Unable to load workspace rules: %s", error->message ); + return G_SOURCE_REMOVE; + } + + JsonArray* rules_array = json_node_get_array( rules_node ); + for ( guint i = 0; i < json_array_get_length( rules_array ); ++i ) + { + JsonObject* rule_object = json_array_get_object_element( rules_array, i ); + gchar const* rule_id_str = json_object_get_string_member_with_default( rule_object, "workspaceString", NULL ); + if ( id == foobar_workspace_service_parse_id( rule_id_str ) ) + { + is_persistent = json_object_get_boolean_member_with_default( rule_object, "persistent", FALSE ); + break; + } + } + + foobar_workspace_service_add_workspace( data->service, data->service->workspaces, workspace_object, is_persistent ); + foobar_workspace_service_update_active( data->service, data->service->workspaces ); + return G_SOURCE_REMOVE; +} + +// +// Handler for the "destroyworkspacev2" event. +// +// The payload has the format "," where is the numeric ID. +// +gboolean foobar_workspace_service_handle_workspace_destroyed( EventData* data ) +{ + gchar* save; + gchar const* id_str = strtok_r( data->payload, ",", &save ); + gchar const* name = strtok_r( NULL, ",", &save ); + g_return_val_if_fail( id_str && name, G_SOURCE_REMOVE ); + + if ( foobar_workspace_service_is_invalid_name( name ) ) { return G_SOURCE_REMOVE; } + gint64 id = foobar_workspace_service_parse_id( id_str ); + + GListStore* workspaces = data->service->workspaces; + guint index; + FoobarWorkspace* workspace = foobar_workspace_service_find_workspace( G_LIST_MODEL( workspaces ), id, &index ); + if ( workspace ) + { + workspace->service = NULL; + g_list_store_remove( workspaces, index ); + } + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "moveworkspacev2" event. +// +// The payload has the format ",," where is the numeric ID of the workspace. +// +gboolean foobar_workspace_service_handle_workspace_moved( EventData* data ) +{ + gchar* save; + gchar const* workspace_id_str = strtok_r( data->payload, ",", &save ); + gchar const* workspace_name = strtok_r( NULL, ",", &save ); + gchar const* monitor_name = strtok_r( NULL, ",", &save ); + g_return_val_if_fail( workspace_id_str && workspace_name && monitor_name, G_SOURCE_REMOVE ); + + gint64 workspace_id = foobar_workspace_service_parse_id( workspace_id_str ); + FoobarWorkspace* workspace = foobar_workspace_service_find_workspace( + G_LIST_MODEL( data->service->workspaces ), + workspace_id, + NULL ); + if ( workspace ) + { + foobar_workspace_set_monitor( workspace, monitor_name ); + g_signal_emit( data->service, signals[SIGNAL_MONITOR_CONFIGURATION_CHANGED], 0 ); + } + foobar_workspace_service_update_active( data->service, data->service->workspaces ); + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "renameworkspace" event. +// +// The payload has the format "," where is the numeric ID of the workspace. +// +gboolean foobar_workspace_service_handle_workspace_renamed( EventData* data ) +{ + gchar* save; + gchar const* id_str = strtok_r( data->payload, ",", &save ); + gchar const* new_name = strtok_r( NULL, ",", &save ); + g_return_val_if_fail( id_str && new_name, G_SOURCE_REMOVE ); + + gint64 id = foobar_workspace_service_parse_id( id_str ); + FoobarWorkspace* workspace = foobar_workspace_service_find_workspace( + G_LIST_MODEL( data->service->workspaces ), + id, + NULL ); + if ( workspace ) + { + foobar_workspace_set_name( workspace, new_name ); + gtk_sorter_changed( + gtk_sort_list_model_get_sorter( data->service->sorted_workspaces ), + GTK_SORTER_CHANGE_DIFFERENT ); + } + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "workspace" event. +// +gboolean foobar_workspace_service_handle_workspace_activated( EventData* data ) +{ + foobar_workspace_service_update_active( data->service, data->service->workspaces ); + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "activespecial" event. +// +gboolean foobar_workspace_service_handle_special_workspace_activated( EventData* data ) +{ + foobar_workspace_service_update_active( data->service, data->service->workspaces ); + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "focusedmon" event. +// +gboolean foobar_workspace_service_handle_monitor_focused( EventData* data ) +{ + foobar_workspace_service_update_active( data->service, data->service->workspaces ); + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "urgent" event. +// +// The payload has the format "
" where
is the sender window's address. +// +gboolean foobar_workspace_service_handle_window_urgent( EventData* data ) +{ + gchar const* address = data->payload; + + // Find the client with the given address. + + g_autoptr( GError ) error = NULL; + g_autoptr( JsonNode ) clients_node = foobar_workspace_service_send_json_request( + data->service, + "j/clients", + &error ); + if ( !clients_node ) + { + g_warning( "Unable to load clients: %s", error->message ); + return G_SOURCE_REMOVE; + } + + JsonArray* clients_array = json_node_get_array( clients_node ); + JsonObject* client_object = NULL; + for ( guint i = 0; i < json_array_get_length( clients_array ); ++i ) + { + JsonObject* it = json_array_get_object_element( clients_array, i ); + gchar const* it_address = json_object_get_string_member_with_default( it, "address", "" ); + if ( g_str_has_suffix( it_address, address ) ) + { + client_object = it; + break; + } + } + + if ( !client_object ) { return G_SOURCE_REMOVE; } + + // Find the workspace this window belongs to and mark it as "urgent". + + JsonObject* workspace_object = json_object_get_object_member( client_object, "workspace" ); + gint64 id = json_object_get_int_member( workspace_object, "id" ); + FoobarWorkspace* workspace = foobar_workspace_service_find_workspace( + G_LIST_MODEL( data->service->workspaces ), + id, + NULL ); + if ( workspace ) + { + FoobarWorkspaceFlags flags = foobar_workspace_get_flags( workspace ) | FOOBAR_WORKSPACE_FLAGS_URGENT; + foobar_workspace_set_flags( workspace, flags ); + } + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "configreloaded" event. +// +gboolean foobar_workspace_service_handle_config_reloaded( EventData* data ) +{ + foobar_workspace_service_initialize( data->service ); + + return G_SOURCE_REMOVE; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Implementation of the event thread. +// +// This thread listens on hyprland's event socket (socket.2.sock) for incoming event notifications and then submits +// their registered handlers (if any) to the main loop. +// +gpointer foobar_workspace_service_event_thread_func( gpointer userdata ) +{ + FoobarWorkspaceService* self = (FoobarWorkspaceService*)userdata; + + g_autoptr( GError ) error = NULL; + g_autoptr( GSocket ) sock = g_socket_new( + G_SOCKET_FAMILY_UNIX, + G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_DEFAULT, + &error ); + if ( !sock ) + { + g_warning( "Unable to create socket for hyprland communication: %s", error->message ); + return NULL; + } + + g_autoptr( GSocketAddress ) addr = g_unix_socket_address_new( self->rx_path ); + if ( !g_socket_connect( sock, addr, self->event_cancellable, &error ) && + !g_cancellable_is_cancelled( self->event_cancellable ) ) + { + g_warning( "Unable to connect socket to hyprland: %s", error->message ); + return NULL; + } + + // Each event is sent as a complete line, separated by a newline character. + + GString* prev_buf = NULL; // for longer, incomplete messages + gchar buf[1024]; + while ( !g_cancellable_is_cancelled( self->event_cancellable ) ) + { + // Synchronously receive data from the socket, but support cancellation using Gio. + + gssize received = g_socket_receive( sock, buf, sizeof( buf ), self->event_cancellable, &error ); + if ( error && !g_cancellable_is_cancelled( self->event_cancellable ) ) + { + g_warning( "Unable to receive events from hyprland: %s", error->message ); + } + if ( received <= 0 ) { break; } + + gchar* start = buf; + gchar* end = start + received; + while ( start < end ) + { + gchar* next_newline = g_strstr_len( start, end - start, "\n" ); + if ( !next_newline ) + { + // Incomplete message (missing newline) -> store it in the buffer + + prev_buf = ( prev_buf != NULL ) + ? g_string_append_len( prev_buf, start, end - start ) + : g_string_new_len( start, end - start ); + break; + } + + if ( prev_buf ) + { + // Prepend buffered data before the actual data. + + prev_buf = g_string_append_len( prev_buf, start, next_newline - start ); + foobar_workspace_service_dispatch_event( self, prev_buf->str ); + + g_string_free( prev_buf, TRUE ); + prev_buf = NULL; + } + else + { + // Skip allocation and use the buffer contents on the stack directly. + + *next_newline = '\0'; + foobar_workspace_service_dispatch_event( self, start ); + } + + start = next_newline + 1; + } + } + + if ( prev_buf ) { g_string_free( prev_buf, TRUE ); } + + return NULL; +} + +// +// Called when an entire event has been received on the event thread. +// +// This will look through the registered handlers and (if found) invoke the event's handler on the main thread. +// +void foobar_workspace_service_dispatch_event( + FoobarWorkspaceService* self, + gchar* message ) +{ + gchar* delimiter = strstr( message, ">>" ); + if ( !delimiter ) + { + g_warning( "Received an invalid message from hyprland: %s", message ); + return; + } + + *delimiter = '\0'; + gchar* name = message; + gchar* payload = delimiter + 2; + for ( gsize i = 0; i < G_N_ELEMENTS( event_handlers ); ++i ) + { + EventHandler handler = event_handlers[i]; + if ( !strcmp( name, handler.name ) ) + { + g_idle_add_full( + G_PRIORITY_DEFAULT, + G_SOURCE_FUNC( handler.fn ), + event_data_new( self, name, payload ), + (GDestroyNotify)event_data_free ); + break; + } + } +} + +// +// Synchronously send a command to hyprland's control socket (socket.sock), returning the entire response. +// +gchar* foobar_workspace_service_send_request( + FoobarWorkspaceService* self, + gchar const* request, + GError** error ) +{ + g_autoptr( GSocket ) sock = g_socket_new( + G_SOCKET_FAMILY_UNIX, + G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_DEFAULT, + error ); + if ( !sock ) { return NULL; } + + g_autoptr( GSocketAddress ) addr = g_unix_socket_address_new( self->tx_path ); + if ( !g_socket_connect( sock, addr, NULL, error ) ) { return NULL; } + + if ( g_socket_send( sock, request, strlen( request ), NULL, error ) == -1 ) { return NULL; } + + GString* str = g_string_sized_new( 0 ); + gchar buf[8192]; + while ( TRUE ) + { + gssize received = g_socket_receive( sock, buf, sizeof( buf ), self->event_cancellable, error ); + if ( received < 0 ) { return g_string_free( str, TRUE ); } + + if ( received == 0 ) { break; } + str = g_string_append_len( str, buf, received ); + } + + return g_string_free( str, FALSE ); +} + +// +// Synchronously send a command to hyprland's control socket (socket.sock), returning the parsed the JSON response. +// +JsonNode* foobar_workspace_service_send_json_request( + FoobarWorkspaceService* self, + gchar const* request, + GError** error ) +{ + g_autofree gchar* json = foobar_workspace_service_send_request( self, request, error ); + if ( !json ) { return NULL; } + + g_autoptr( JsonParser ) parser = json_parser_new( ); + if ( !json_parser_load_from_data( parser, json, (gssize)strlen( json ), error ) ) { return NULL; } + + return json_parser_steal_root( parser ); +} + +// +// Refresh the list of workspaces by actively sending a request to hyprland. +// +void foobar_workspace_service_initialize( FoobarWorkspaceService* self ) +{ + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->workspaces ) ); ++i ) + { + FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( self->workspaces ), i ); + workspace->service = NULL; + } + g_list_store_remove_all( self->workspaces ); + g_autoptr( GListStore ) workspaces = g_list_store_new( FOOBAR_TYPE_WORKSPACE ); + + { + g_autoptr( GError ) error = NULL; + g_autoptr( JsonNode ) workspaces_node = foobar_workspace_service_send_json_request( + self, + "j/workspaces", + &error ); + if ( !workspaces_node ) + { + g_warning( "Unable to load workspaces: %s", error->message ); + return; + } + + JsonArray* workspaces_array = json_node_get_array( workspaces_node ); + for ( guint i = 0; i < json_array_get_length( workspaces_array ); ++i ) + { + JsonObject* workspace_object = json_array_get_object_element( workspaces_array, i ); + foobar_workspace_service_add_workspace( self, workspaces, workspace_object, FALSE ); + } + } + + { + // Load configured persistent workspaces. + + g_autoptr( GError ) error = NULL; + g_autoptr( JsonNode ) rules_node = foobar_workspace_service_send_json_request( + self, + "j/workspacerules", + &error ); + if ( rules_node ) + { + JsonArray* rules_array = json_node_get_array( rules_node ); + for ( guint i = 0; i < json_array_get_length( rules_array ); ++i ) + { + JsonObject* rule_object = json_array_get_object_element( rules_array, i ); + gchar const* name = json_object_get_string_member_with_default( rule_object, "workspaceString", NULL ); + if ( !name ) + { + g_warning( "Found an invalid workspaceString value, skipping." ); + continue; + } + + if ( !json_object_get_boolean_member_with_default( rule_object, "persistent", FALSE ) ) { continue; } + + gint64 id = foobar_workspace_service_parse_id( name ); + json_object_set_int_member( rule_object, "id", id ); + json_object_set_string_member( rule_object, "name", name ); + + foobar_workspace_service_add_workspace( self, workspaces, rule_object, TRUE ); + } + } + else { g_warning( "Unable to load workspace rules: %s", error->message ); } + } + + foobar_workspace_service_update_active( self, workspaces ); + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( workspaces ) ); ++i ) + { + FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( workspaces ), i ); + g_list_store_append( self->workspaces, workspace ); + } +} + +// +// Update the currently active/visible workspaces by actively sending a request to hyprland. +// +void foobar_workspace_service_update_active( + FoobarWorkspaceService* self, + GListStore* workspaces ) +{ + g_autoptr( GArray ) visible_workspace_ids = g_array_new( FALSE, FALSE, sizeof( gint64 ) ); + gint64 active_workspace_id = 0; + gint64 active_special_workspace_id = 0; + + { + // Get all active workspaces + + g_autoptr( GError ) error = NULL; + g_autoptr( JsonNode ) monitors_node = foobar_workspace_service_send_json_request( + self, + "j/monitors", + &error ); + if ( monitors_node ) + { + JsonArray* monitors_array = json_node_get_array( monitors_node ); + for ( guint i = 0; i < json_array_get_length( monitors_array ); ++i ) + { + JsonObject* monitor_object = json_array_get_object_element( monitors_array, i ); + gint64 workspace_id = 0; + gint64 special_workspace_id = 0; + + if ( json_object_has_member( monitor_object, "activeWorkspace" ) ) + { + JsonObject* workspace_object = json_object_get_object_member( monitor_object, "activeWorkspace" ); + workspace_id = json_object_get_int_member_with_default( workspace_object, "id", 0 ); + if ( workspace_id ) { g_array_append_val( visible_workspace_ids, workspace_id ); } + } + + if ( json_object_has_member( monitor_object, "specialWorkspace" ) ) + { + JsonObject* workspace_object = json_object_get_object_member( monitor_object, "specialWorkspace" ); + special_workspace_id = json_object_get_int_member_with_default( workspace_object, "id", 0 ); + if ( special_workspace_id ) { g_array_append_val( visible_workspace_ids, special_workspace_id ); } + } + + // If this is the focused monitor, then the visible workspace is also the active one. + + if ( json_object_get_boolean_member_with_default( monitor_object, "focused", FALSE ) ) + { + active_workspace_id = workspace_id; + active_special_workspace_id = special_workspace_id; + } + } + } + else { g_warning( "Unable to load monitors: %s", error->message ); } + } + + // Sort visible workspaces to allow binary search. + + g_array_sort( visible_workspace_ids, foobar_workspace_service_compare_ids ); + + // Go through all workspaces and update their state. + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( workspaces ) ); ++i ) + { + FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( workspaces ), i ); + FoobarWorkspaceFlags flags = foobar_workspace_get_flags( workspace ); + gint64 id = foobar_workspace_get_id( workspace ); + + // Active + gboolean is_active = id == active_workspace_id || id == active_special_workspace_id; + flags = is_active ? flags | FOOBAR_WORKSPACE_FLAGS_ACTIVE : flags & ~FOOBAR_WORKSPACE_FLAGS_ACTIVE; + + // Disable urgency if workspace is active + if ( is_active ) { flags = flags & ~FOOBAR_WORKSPACE_FLAGS_URGENT; } + + // Visible + gboolean is_visible = g_array_binary_search( + visible_workspace_ids, + &id, + foobar_workspace_service_compare_ids, + NULL ); + flags = is_visible ? flags | FOOBAR_WORKSPACE_FLAGS_VISIBLE : flags & ~FOOBAR_WORKSPACE_FLAGS_VISIBLE; + + foobar_workspace_set_flags( workspace, flags ); + } +} + +// +// Add or update a workspace described by a JSON object. +// +void foobar_workspace_service_add_workspace( + FoobarWorkspaceService* self, + GListStore* workspaces, + JsonObject* workspace_object, + gboolean is_persistent ) +{ + gchar const* name = json_object_get_string_member( workspace_object, "name" ); + gint64 id = json_object_get_int_member( workspace_object, "id" ); + gboolean is_special = g_str_has_prefix( name, "special" ); + if ( g_str_has_prefix( name, "name:" ) ) { name += strlen( "name:" ); } + else if ( g_str_has_prefix( name, "special:" ) ) { name += strlen( "special:" ); } + + FoobarWorkspace* existing = foobar_workspace_service_find_workspace( G_LIST_MODEL( workspaces ), id, NULL ); + if ( existing ) + { + // Only update persistence. + + FoobarWorkspaceFlags flags = foobar_workspace_get_flags( existing ); + flags = is_persistent ? flags | FOOBAR_WORKSPACE_FLAGS_PERSISTENT : flags & ~FOOBAR_WORKSPACE_FLAGS_PERSISTENT; + foobar_workspace_set_flags( existing, flags ); + } + else + { + // Found a new workspace -> create a new object for it. + + FoobarWorkspaceFlags flags = FOOBAR_WORKSPACE_FLAGS_NONE; + flags = is_persistent ? flags | FOOBAR_WORKSPACE_FLAGS_PERSISTENT : flags; + flags = is_special ? flags | FOOBAR_WORKSPACE_FLAGS_SPECIAL : flags; + + g_autoptr( FoobarWorkspace ) workspace = foobar_workspace_new( self ); + foobar_workspace_set_id( workspace, id ); + foobar_workspace_set_name( workspace, name ); + foobar_workspace_set_monitor( workspace, json_object_get_string_member( workspace_object, "monitor" ) ); + foobar_workspace_set_flags( workspace, flags ); + g_list_store_append( workspaces, workspace ); + } +} + +// +// Find an existing workspace object by its ID. +// +FoobarWorkspace* foobar_workspace_service_find_workspace( + GListModel* workspaces, + gint64 id, + guint* out_index ) +{ + for ( guint i = 0; i < g_list_model_get_n_items( workspaces ); ++i ) + { + FoobarWorkspace* workspace = g_list_model_get_item( workspaces, i ); + if ( foobar_workspace_get_id( workspace ) == id ) + { + if ( out_index ) { *out_index = i; } + return workspace; + } + } + + return NULL; +} + +// +// Hyprland's IPC sometimes reports the creation of workspaces strangely named `special:special:`. This +// function checks for that and is used to avoid creating (and then removing) such workspaces. +// +// See hyprwm/Hyprland#3424 for more info. +// +gboolean foobar_workspace_service_is_invalid_name( gchar const* name ) +{ + return g_str_has_prefix( name, "special:special:" ); +} + +// +// Parse an ID string and return its numeric code. +// +gint64 foobar_workspace_service_parse_id( gchar const* str ) +{ + if ( !g_strcmp0( str, "special" ) ) { return -99; } + + return strtoll( str, NULL, 10 ); +} + +// +// Get the base path for hyprland socket files. +// +gchar* foobar_workspace_service_get_hyprland_base_path( void ) +{ + gchar const* instance_signature = g_getenv( "HYPRLAND_INSTANCE_SIGNATURE" ); + if ( !instance_signature ) + { + return NULL; + } + + // for Hyprland >= 0.40 + gchar* result = g_strdup_printf( "%s/hypr/%s", g_get_user_runtime_dir( ), instance_signature ); + if ( g_file_test( result, G_FILE_TEST_EXISTS ) ) + { + return result; + } + + // for Hyprland < 0.40 + g_free( result ); + result = g_strdup_printf( "%s/hypr/%s", g_get_tmp_dir( ), instance_signature ); + if ( g_file_test( result, G_FILE_TEST_EXISTS ) ) + { + return result; + } + + g_free( result ); + return NULL; +} + +// +// Comparison function for two workspace IDs, used for binary search. +// +gint foobar_workspace_service_compare_ids( + gconstpointer a, + gconstpointer b ) +{ + gint64 const* a_int = a; + gint64 const* b_int = b; + + if ( *a_int < *b_int ) { return -1; } + if ( *a_int > *b_int ) { return 1; } + return 0; +} + +// +// Sorting callback for workspaces. Items are sorted to be in the following order: +// 1. normal +// 2. named persistent +// 3. named +// 4. special +// 5. named special +// +gint foobar_workspace_service_sort_func( + gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ) +{ + (void)userdata; + + FoobarWorkspace* workspace_a = (FoobarWorkspace*)item_a; + FoobarWorkspace* workspace_b = (FoobarWorkspace*)item_b; + gint64 id_a = foobar_workspace_get_id( workspace_a ); + gint64 id_b = foobar_workspace_get_id( workspace_b ); + gboolean special_a = ( foobar_workspace_get_flags( workspace_a ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) != 0; + gboolean special_b = ( foobar_workspace_get_flags( workspace_b ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) != 0; + gboolean named_a = id_a < 0; + gboolean named_b = id_b < 0; + + // both normal (includes numbered persistent) => sort by ID + if ( !named_a && !named_b ) + { + if ( id_a < id_b ) { return -1; } + if ( id_a > id_b ) { return 1; } + return 0; + } + + // one normal, one special => normal first + if ( special_a && !special_b ) { return 1; } + if ( !special_a && special_b ) { return -1; } + + // only one normal, one named + if ( named_a && !named_b ) { return -1; } + if ( !named_a && named_b ) { return 1; } + + // both special + if ( special_a && special_b ) + { + // if one is -99 => put it last + if ( id_a == -99 && id_b != -99 ) { return 1; } + if ( id_a != -99 && id_b == -99 ) { return -1; } + } + + return g_strcmp0( foobar_workspace_get_name( workspace_a ), foobar_workspace_get_name( workspace_b ) ); +} + +// +// Create a new EventData structure. +// +static EventData* event_data_new( + FoobarWorkspaceService* service, + gchar const* name, + gchar const* payload ) +{ + EventData* self = g_new0( EventData, 1 ); + self->service = g_object_ref( service ); + self->name = g_strdup( name ); + self->payload = g_strdup( payload ); + return self; +} + +// +// Destroy an EventData structure. +// +static void event_data_free( EventData* self ) +{ + if ( self ) + { + g_object_unref( self->service ); + g_free( self->name ); + g_free( self->payload ); + g_free( self ); + } +} diff --git a/csrc/services/workspace-service.h b/csrc/services/workspace-service.h new file mode 100644 index 0000000..c80e7dc --- /dev/null +++ b/csrc/services/workspace-service.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_WORKSPACE_FLAGS foobar_workspace_flags_get_type( ) +#define FOOBAR_TYPE_WORKSPACE foobar_workspace_get_type( ) +#define FOOBAR_TYPE_WORKSPACE_SERVICE foobar_workspace_service_get_type( ) + +typedef enum +{ + FOOBAR_WORKSPACE_FLAGS_NONE = 0, + // Indicates that the workspace is a globally active one (i.e. visible on the focused monitor). + FOOBAR_WORKSPACE_FLAGS_ACTIVE = 1 << 0, + // Indicates that the workspace is currently visible. + FOOBAR_WORKSPACE_FLAGS_VISIBLE = 1 << 1, + // Indicates that the workspace is a special workspace. + FOOBAR_WORKSPACE_FLAGS_SPECIAL = 1 << 2, + // Indicates that the workspace is persistent (i.e. configured in the config file). + FOOBAR_WORKSPACE_FLAGS_PERSISTENT = 1 << 3, + // Indicates that the workspace requires an urgent action (e.g. a notification). + FOOBAR_WORKSPACE_FLAGS_URGENT = 1 << 4, +} FoobarWorkspaceFlags; + +GType foobar_workspace_flags_get_type( void ); + +G_DECLARE_FINAL_TYPE( FoobarWorkspace, foobar_workspace, FOOBAR, WORKSPACE, GObject ) + +gint64 foobar_workspace_get_id ( FoobarWorkspace* self ); +gchar const* foobar_workspace_get_name ( FoobarWorkspace* self ); +gchar const* foobar_workspace_get_monitor( FoobarWorkspace* self ); +FoobarWorkspaceFlags foobar_workspace_get_flags ( FoobarWorkspace* self ); +void foobar_workspace_activate ( FoobarWorkspace* self ); + +G_DECLARE_FINAL_TYPE( FoobarWorkspaceService, foobar_workspace_service, FOOBAR, WORKSPACE_SERVICE, GObject ) + +FoobarWorkspaceService* foobar_workspace_service_new ( void ); +GListModel* foobar_workspace_service_get_workspaces( FoobarWorkspaceService* self ); + +G_END_DECLS diff --git a/csrc/utils.c b/csrc/utils.c new file mode 100644 index 0000000..dae3e65 --- /dev/null +++ b/csrc/utils.c @@ -0,0 +1,25 @@ +#include "utils.h" +#include + +// +// Return the path for a file in the user's XDG cache directory. The result should be freed using g_free. +// +// This will also ensure that the application's cache directory exists. +// +gchar* foobar_get_cache_path( gchar const* filename ) +{ + g_autoptr( GError ) error = NULL; + g_autofree gchar* directory_path = g_strdup_printf( "%s/foobar", g_get_user_cache_dir( ) ); + + if ( !g_file_test( directory_path, G_FILE_TEST_EXISTS ) ) + { + g_autoptr( GFile ) cache_dir = g_file_new_for_path( directory_path ); + if ( !g_file_make_directory_with_parents( cache_dir, NULL, &error ) ) + { + g_warning( "Unable to create cache directory: %s", error->message ); + return NULL; + } + } + + return g_strdup_printf( "%s/%s", directory_path, filename ); +} diff --git a/csrc/utils.h b/csrc/utils.h new file mode 100644 index 0000000..0cd6e38 --- /dev/null +++ b/csrc/utils.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +gchar* foobar_get_cache_path( gchar const* filename ); + +G_END_DECLS diff --git a/csrc/widgets/control-center/control-button.c b/csrc/widgets/control-center/control-button.c new file mode 100644 index 0000000..5b6d991 --- /dev/null +++ b/csrc/widgets/control-center/control-button.c @@ -0,0 +1,496 @@ +#include "widgets/control-center/control-button.h" + +// +// FoobarControlButton: +// +// A button in the "controls" section of the control center. This can be a regular button or a toggle button with an +// optional secondary button to toggle a details view. +// + +struct _FoobarControlButton +{ + GtkWidget parent_instance; + gchar* icon_name; + gchar* label; + gboolean can_expand; + gboolean can_toggle; + gboolean is_expanded; + gboolean is_toggled; +}; + +enum +{ + PROP_ICON_NAME = 1, + PROP_LABEL, + PROP_CAN_EXPAND, + PROP_CAN_TOGGLE, + PROP_IS_EXPANDED, + PROP_IS_TOGGLED, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_control_button_class_init ( FoobarControlButtonClass* klass ); +static void foobar_control_button_init ( FoobarControlButton* self ); +static void foobar_control_button_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_control_button_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_control_button_dispose ( GObject* object ); +static void foobar_control_button_finalize ( GObject* object ); +static void foobar_control_button_handle_primary_clicked( GtkButton* button, + gpointer userdata ); +static void foobar_control_button_handle_expand_clicked ( GtkButton* button, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarControlButton, foobar_control_button, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for control buttons. +// +void foobar_control_button_class_init( FoobarControlButtonClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + gtk_widget_class_set_css_name( widget_klass, "control-button" ); + gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_control_button_get_property; + object_klass->set_property = foobar_control_button_set_property; + object_klass->dispose = foobar_control_button_dispose; + object_klass->finalize = foobar_control_button_finalize; + + props[PROP_ICON_NAME] = g_param_spec_string( + "icon-name", + "Icon Name", + "Name of the icon to be displayed next to the label.", + NULL, + G_PARAM_READWRITE ); + props[PROP_LABEL] = g_param_spec_string( + "label", + "Label", + "The button's main label.", + NULL, + G_PARAM_READWRITE ); + props[PROP_CAN_EXPAND] = g_param_spec_boolean( + "can-expand", + "Can Expand", + "Indicates whether some details are hidden and can be revealed by a toggle button next to the main action.", + FALSE, + G_PARAM_READWRITE ); + props[PROP_CAN_TOGGLE] = g_param_spec_boolean( + "can-toggle", + "Can Toggle", + "Indicates whether the main button can be toggled.", + FALSE, + G_PARAM_READWRITE ); + props[PROP_IS_EXPANDED] = g_param_spec_boolean( + "is-expanded", + "Is Expanded", + "Indicates whether additional details should be shown.", + FALSE, + G_PARAM_READWRITE ); + props[PROP_IS_TOGGLED] = g_param_spec_boolean( + "is-toggled", + "Is Toggled", + "Indicates whether the main button is currently toggled.", + FALSE, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for control buttons. +// +void foobar_control_button_init( FoobarControlButton* self ) +{ + GtkWidget* icon = gtk_image_new( ); + gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); + g_object_bind_property( + self, + "icon-name", + icon, + "icon-name", + G_BINDING_SYNC_CREATE ); + + GtkWidget* label = gtk_label_new( NULL ); + gtk_widget_set_valign( label, GTK_ALIGN_CENTER ); + gtk_widget_set_hexpand( label, TRUE ); + gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); + gtk_label_set_xalign( GTK_LABEL( label ), 0 ); + g_object_bind_property( + self, + "label", + label, + "label", + G_BINDING_SYNC_CREATE ); + + GtkWidget* content = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); + gtk_box_append( GTK_BOX( content ), icon ); + gtk_box_append( GTK_BOX( content ), label ); + + GtkWidget* primary_button = gtk_button_new( ); + gtk_widget_add_css_class( primary_button, "primary" ); + gtk_widget_set_hexpand( primary_button, TRUE ); + gtk_button_set_child( GTK_BUTTON( primary_button ), content ); + g_object_bind_property( + self, + "can-toggle", + primary_button, + "sensitive", + G_BINDING_SYNC_CREATE ); + g_signal_connect( primary_button, "clicked", G_CALLBACK( foobar_control_button_handle_primary_clicked ), self ); + + GtkWidget* expand_button = gtk_button_new_from_icon_name( "fluent-chevron-right-symbolic" ); + gtk_widget_add_css_class( expand_button, "expand" ); + g_object_bind_property( + self, + "can-expand", + expand_button, + "visible", + G_BINDING_SYNC_CREATE ); + g_signal_connect( expand_button, "clicked", G_CALLBACK( foobar_control_button_handle_expand_clicked ), self ); + + GtkLayoutManager* layout = gtk_widget_get_layout_manager( GTK_WIDGET( self ) ); + gtk_orientable_set_orientation( GTK_ORIENTABLE( layout ), GTK_ORIENTATION_HORIZONTAL ); + gtk_widget_insert_before( primary_button, GTK_WIDGET( self ), NULL ); + gtk_widget_insert_before( expand_button, GTK_WIDGET( self ), NULL ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_control_button_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarControlButton* self = (FoobarControlButton*)object; + + switch ( prop_id ) + { + case PROP_ICON_NAME: + g_value_set_string( value, foobar_control_button_get_icon_name( self ) ); + break; + case PROP_LABEL: + g_value_set_string( value, foobar_control_button_get_label( self ) ); + break; + case PROP_CAN_EXPAND: + g_value_set_boolean( value, foobar_control_button_can_expand( self ) ); + break; + case PROP_CAN_TOGGLE: + g_value_set_boolean( value, foobar_control_button_can_toggle( self ) ); + break; + case PROP_IS_EXPANDED: + g_value_set_boolean( value, foobar_control_button_is_expanded( self ) ); + break; + case PROP_IS_TOGGLED: + g_value_set_boolean( value, foobar_control_button_is_toggled( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Property setter implementation, mapping a property id to a method. +// +void foobar_control_button_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarControlButton* self = (FoobarControlButton*)object; + + switch ( prop_id ) + { + case PROP_ICON_NAME: + foobar_control_button_set_icon_name( self, g_value_get_string( value ) ); + break; + case PROP_LABEL: + foobar_control_button_set_label( self, g_value_get_string( value ) ); + break; + case PROP_CAN_EXPAND: + foobar_control_button_set_can_expand( self, g_value_get_boolean( value ) ); + break; + case PROP_CAN_TOGGLE: + foobar_control_button_set_can_toggle( self, g_value_get_boolean( value ) ); + break; + case PROP_IS_EXPANDED: + foobar_control_button_set_expanded( self, g_value_get_boolean( value ) ); + break; + case PROP_IS_TOGGLED: + foobar_control_button_set_toggled( self, g_value_get_boolean( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for control buttons. +// +void foobar_control_button_dispose( GObject* object ) +{ + FoobarControlButton* self = (FoobarControlButton*)object; + + GtkWidget* child; + while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) + { + gtk_widget_unparent( child ); + } + + G_OBJECT_CLASS( foobar_control_button_parent_class )->dispose( object ); +} + +// +// Instance cleanup for control buttons. +// +void foobar_control_button_finalize( GObject* object ) +{ + FoobarControlButton* self = (FoobarControlButton*)object; + + g_clear_pointer( &self->icon_name, g_free ); + g_clear_pointer( &self->label, g_free ); + + G_OBJECT_CLASS( foobar_control_button_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new control button instance. +// +GtkWidget* foobar_control_button_new( void ) +{ + return g_object_new( FOOBAR_TYPE_CONTROL_BUTTON, NULL ); +} + +// +// Get the name of the icon to be displayed next to the label. +// +gchar const* foobar_control_button_get_icon_name( FoobarControlButton* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), NULL ); + return self->icon_name; +} + +// +// Get the button's main label. +// +gchar const* foobar_control_button_get_label( FoobarControlButton* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), NULL ); + return self->label; +} + +// +// Check whether some details are hidden and can be revealed by a toggle button next to the main action. +// +gboolean foobar_control_button_can_expand( FoobarControlButton* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE ); + return self->can_expand; +} + +// +// Check whether the main button can be toggled. +// +gboolean foobar_control_button_can_toggle( FoobarControlButton* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE ); + return self->can_toggle; +} + +// +// Check whether additional details should currently be shown. +// +gboolean foobar_control_button_is_expanded( FoobarControlButton* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE ); + return self->is_expanded; +} + +// +// Check whether the main button is currently toggled. +// +gboolean foobar_control_button_is_toggled( FoobarControlButton* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE ); + return self->is_toggled; +} + +// +// Update the name of the icon to be displayed next to the label. +// +void foobar_control_button_set_icon_name( + FoobarControlButton* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); + + if ( g_strcmp0( self->icon_name, value ) ) + { + g_clear_pointer( &self->icon_name, g_free ); + self->icon_name = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ICON_NAME] ); + } +} + +// +// Update the button's main label. +// +void foobar_control_button_set_label( + FoobarControlButton* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); + + if ( g_strcmp0( self->label, value ) ) + { + g_clear_pointer( &self->label, g_free ); + self->label = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] ); + } +} + +// +// Update whether some details are hidden and can be revealed by a toggle button next to the main action. +// +void foobar_control_button_set_can_expand( + FoobarControlButton* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); + + if ( !value ) { foobar_control_button_set_expanded( self, FALSE ); } + + value = !!value; + if ( self->can_expand != value ) + { + self->can_expand = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_EXPAND] ); + } +} + +// +// Update whether the main button can be toggled. +// +void foobar_control_button_set_can_toggle( + FoobarControlButton* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); + + if ( !value ) { foobar_control_button_set_toggled( self, FALSE ); } + + value = !!value; + if ( self->can_toggle != value ) + { + self->can_toggle = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_TOGGLE] ); + } +} + +// +// Update whether additional details are currently shown. +// +void foobar_control_button_set_expanded( + FoobarControlButton* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); + + value = value && self->can_expand; + if ( self->is_expanded != value ) + { + self->is_expanded = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] ); + + if ( self->is_expanded ) + { + gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" ); + } + else + { + gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" ); + } + } +} + +// +// Update whether the main button is currently toggled. +// +void foobar_control_button_set_toggled( + FoobarControlButton* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); + + value = value && self->can_toggle; + if ( self->is_toggled != value ) + { + self->is_toggled = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_TOGGLED] ); + + if ( self->is_toggled ) + { + gtk_widget_add_css_class( GTK_WIDGET( self ), "toggled" ); + } + else + { + gtk_widget_remove_css_class( GTK_WIDGET( self ), "toggled" ); + } + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called when the primary button was clicked. +// +// If allowed, this will toggle the button. +// +void foobar_control_button_handle_primary_clicked( GtkButton* button, gpointer userdata ) +{ + (void)button; + FoobarControlButton* self = (FoobarControlButton*)userdata; + + if ( foobar_control_button_can_toggle( self ) ) + { + foobar_control_button_set_toggled( self, !foobar_control_button_is_toggled( self ) ); + } +} + +// +// Called when the expand button was clicked. +// +// If allowed, this will toggle the "expanded" state of the button. +// +void foobar_control_button_handle_expand_clicked( GtkButton* button, gpointer userdata ) +{ + (void)button; + FoobarControlButton* self = (FoobarControlButton*)userdata; + + if ( foobar_control_button_can_expand( self ) ) + { + foobar_control_button_set_expanded( self, !foobar_control_button_is_expanded( self ) ); + } +} diff --git a/csrc/widgets/control-center/control-button.h b/csrc/widgets/control-center/control-button.h new file mode 100644 index 0000000..d4c3737 --- /dev/null +++ b/csrc/widgets/control-center/control-button.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_CONTROL_BUTTON foobar_control_button_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarControlButton, foobar_control_button, FOOBAR, CONTROL_BUTTON, GtkWidget ) + +GtkWidget* foobar_control_button_new ( void ); +gchar const* foobar_control_button_get_icon_name ( FoobarControlButton* self ); +gchar const* foobar_control_button_get_label ( FoobarControlButton* self ); +gboolean foobar_control_button_can_expand ( FoobarControlButton* self ); +gboolean foobar_control_button_can_toggle ( FoobarControlButton* self ); +gboolean foobar_control_button_is_expanded ( FoobarControlButton* self ); +gboolean foobar_control_button_is_toggled ( FoobarControlButton* self ); +void foobar_control_button_set_icon_name ( FoobarControlButton* self, + gchar const* value ); +void foobar_control_button_set_label ( FoobarControlButton* self, + gchar const* value ); +void foobar_control_button_set_can_expand( FoobarControlButton* self, + gboolean value ); +void foobar_control_button_set_can_toggle( FoobarControlButton* self, + gboolean value ); +void foobar_control_button_set_expanded ( FoobarControlButton* self, + gboolean value ); +void foobar_control_button_set_toggled ( FoobarControlButton* self, + gboolean value ); + +G_END_DECLS diff --git a/csrc/widgets/control-center/control-details-item.c b/csrc/widgets/control-center/control-details-item.c new file mode 100644 index 0000000..cb54dd3 --- /dev/null +++ b/csrc/widgets/control-center/control-details-item.c @@ -0,0 +1,307 @@ +#include "widgets/control-center/control-details-item.h" + +// +// FoobarControlDetailsAccessory: +// +// The accessory shown next to an item in the control center details list (if any). +// + +G_DEFINE_ENUM_TYPE( + FoobarControlDetailsAccessory, + foobar_control_details_accessory, + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE, "none" ), + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS, "progress" ), + G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED, "checked" ) ) + +// +// FoobarControlDetailsItem: +// +// Standard row layout for items in a FoobarControlDetails list. The rows are made up of a label and an accessory that +// is shown or hidden depending on the item's state. +// + +struct _FoobarControlDetailsItem +{ + GtkWidget parent_instance; + GtkWidget* spinner; + GtkWidget* checkmark; + gchar* label; + FoobarControlDetailsAccessory accessory; +}; + +enum +{ + PROP_LABEL = 1, + PROP_ACCESSORY, + PROP_IS_CHECKED, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_control_details_item_class_init ( FoobarControlDetailsItemClass* klass ); +static void foobar_control_details_item_init ( FoobarControlDetailsItem* self ); +static void foobar_control_details_item_get_property( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_control_details_item_set_property( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_control_details_item_dispose ( GObject* object ); +static void foobar_control_details_item_finalize ( GObject* object ); + +G_DEFINE_FINAL_TYPE( FoobarControlDetailsItem, foobar_control_details_item, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for control details items. +// +void foobar_control_details_item_class_init( FoobarControlDetailsItemClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + gtk_widget_class_set_css_name( widget_klass, "control-details-item" ); + gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_control_details_item_get_property; + object_klass->set_property = foobar_control_details_item_set_property; + object_klass->dispose = foobar_control_details_item_dispose; + object_klass->finalize = foobar_control_details_item_finalize; + + props[PROP_LABEL] = g_param_spec_string( + "label", + "Label", + "Label shown for the item.", + NULL, + G_PARAM_READWRITE ); + props[PROP_ACCESSORY] = g_param_spec_enum( + "accessory", + "Accessory", + "Accessory shown for the item.", + FOOBAR_TYPE_CONTROL_DETAILS_ACCESSORY, + 0, + G_PARAM_READWRITE ); + props[PROP_IS_CHECKED] = g_param_spec_boolean( + "is-checked", + "Is Checked", + "Current checked-state for the item.", + FALSE, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for control details items. +// +void foobar_control_details_item_init( FoobarControlDetailsItem* self ) +{ + GtkWidget* label = gtk_label_new( NULL ); + gtk_widget_set_hexpand( label, TRUE ); + gtk_widget_set_valign( label, GTK_ALIGN_CENTER ); + gtk_label_set_ellipsize( GTK_LABEL( label ), PANGO_ELLIPSIZE_END ); + gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); + gtk_label_set_xalign( GTK_LABEL( label ), 0 ); + gtk_label_set_wrap( GTK_LABEL( label ), FALSE ); + g_object_bind_property( + self, + "label", + label, + "label", + G_BINDING_SYNC_CREATE ); + + self->spinner = gtk_spinner_new( ); + gtk_spinner_set_spinning( GTK_SPINNER( self->spinner ), TRUE ); + gtk_widget_add_css_class( self->spinner, "accessory" ); + gtk_widget_set_valign( self->spinner, GTK_ALIGN_CENTER ); + gtk_widget_set_visible( self->spinner, FALSE ); + + self->checkmark = gtk_image_new_from_icon_name( "fluent-checkmark-symbolic" ); + gtk_widget_add_css_class( self->checkmark, "checkmark" ); + gtk_widget_add_css_class( self->checkmark, "accessory" ); + gtk_widget_set_valign( self->checkmark, GTK_ALIGN_CENTER ); + gtk_widget_set_visible( self->checkmark, FALSE ); + + gtk_widget_insert_before( label, GTK_WIDGET( self ), NULL ); + gtk_widget_insert_before( self->spinner, GTK_WIDGET( self ), NULL ); + gtk_widget_insert_before( self->checkmark, GTK_WIDGET( self ), NULL ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_control_details_item_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object; + + switch ( prop_id ) + { + case PROP_LABEL: + g_value_set_string( value, foobar_control_details_item_get_label( self ) ); + break; + case PROP_ACCESSORY: + g_value_set_enum( value, foobar_control_details_item_get_accessory( self ) ); + break; + case PROP_IS_CHECKED: + g_value_set_boolean( value, foobar_control_details_item_is_checked( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Property setter implementation, mapping a property id to a method. +// +void foobar_control_details_item_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object; + + switch ( prop_id ) + { + case PROP_LABEL: + foobar_control_details_item_set_label( self, g_value_get_string( value ) ); + break; + case PROP_ACCESSORY: + foobar_control_details_item_set_accessory( self, g_value_get_enum( value ) ); + break; + case PROP_IS_CHECKED: + foobar_control_details_item_set_checked( self, g_value_get_boolean( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for control details items. +// +void foobar_control_details_item_dispose( GObject* object ) +{ + FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object; + + GtkWidget* child; + while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) + { + gtk_widget_unparent( child ); + } + + G_OBJECT_CLASS( foobar_control_details_item_parent_class )->dispose( object ); +} + +// +// Instance cleanup for control details items. +// +void foobar_control_details_item_finalize( GObject* object ) +{ + FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object; + + g_clear_pointer( &self->label, g_free ); + + G_OBJECT_CLASS( foobar_control_details_item_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new control details item instance. +// +GtkWidget* foobar_control_details_item_new( void ) +{ + return g_object_new( FOOBAR_TYPE_CONTROL_DETAILS_ITEM, NULL ); +} + +// +// Get the label shown for the item. +// +gchar const* foobar_control_details_item_get_label( FoobarControlDetailsItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), NULL ); + return self->label; +} + +// +// Get the accessory shown for the item. +// +FoobarControlDetailsAccessory foobar_control_details_item_get_accessory( FoobarControlDetailsItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), 0 ); + return self->accessory; +} + +// +// Get the current checked-state for the item. +// +gboolean foobar_control_details_item_is_checked( FoobarControlDetailsItem* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), FALSE ); + return self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED; +} + +// +// Update the label shown for the item. +// +void foobar_control_details_item_set_label( + FoobarControlDetailsItem* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) ); + + if ( g_strcmp0( self->label, value ) ) + { + g_clear_pointer( &self->label, g_free ); + self->label = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] ); + } +} + +// +// Update the current accessory shown for the item. +// +void foobar_control_details_item_set_accessory( + FoobarControlDetailsItem* self, + FoobarControlDetailsAccessory value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) ); + + if ( self->accessory != value ) + { + self->accessory = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ACCESSORY] ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_CHECKED] ); + + gtk_widget_set_visible( self->spinner, self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS ); + gtk_widget_set_visible( self->checkmark, self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED ); + } +} + +// +// Update the current checked-state for the item. +// +void foobar_control_details_item_set_checked( + FoobarControlDetailsItem* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) ); + + FoobarControlDetailsAccessory accessory = value + ? FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED + : FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE; + foobar_control_details_item_set_accessory( self, accessory ); +} diff --git a/csrc/widgets/control-center/control-details-item.h b/csrc/widgets/control-center/control-details-item.h new file mode 100644 index 0000000..ff890f9 --- /dev/null +++ b/csrc/widgets/control-center/control-details-item.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_CONTROL_DETAILS_ACCESSORY foobar_control_details_accessory_get_type( ) +#define FOOBAR_TYPE_CONTROL_DETAILS_ITEM foobar_control_details_item_get_type( ) + +typedef enum +{ + FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE, + FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED, + FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS, +} FoobarControlDetailsAccessory; + +GType foobar_control_details_accessory_get_type( void ); + +G_DECLARE_FINAL_TYPE( FoobarControlDetailsItem, foobar_control_details_item, FOOBAR, CONTROL_DETAILS_ITEM, GtkWidget ) + +GtkWidget* foobar_control_details_item_new ( void ); +gchar const* foobar_control_details_item_get_label ( FoobarControlDetailsItem* self ); +FoobarControlDetailsAccessory foobar_control_details_item_get_accessory( FoobarControlDetailsItem* self ); +gboolean foobar_control_details_item_is_checked ( FoobarControlDetailsItem* self ); +void foobar_control_details_item_set_label ( FoobarControlDetailsItem* self, + gchar const* value ); +void foobar_control_details_item_set_accessory( FoobarControlDetailsItem* self, + FoobarControlDetailsAccessory value ); +void foobar_control_details_item_set_checked ( FoobarControlDetailsItem* self, + gboolean value ); + +G_END_DECLS diff --git a/csrc/widgets/control-center/control-details.c b/csrc/widgets/control-center/control-details.c new file mode 100644 index 0000000..72a90aa --- /dev/null +++ b/csrc/widgets/control-center/control-details.c @@ -0,0 +1,289 @@ +#include "widgets/control-center/control-details.h" + +// +// FoobarControlDetails: +// +// A revealer for details in the "controls" section of the control center. This widget is usually used to embed a list +// view. It takes care of scrolling by embedding the child in a GtkScrolledWindow. +// + +struct _FoobarControlDetails +{ + GtkWidget parent_instance; + GtkWidget* child; + gboolean is_expanded; + gint inset_top; +}; + +enum +{ + PROP_CHILD = 1, + PROP_IS_EXPANDED, + PROP_INSET_TOP, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_control_details_class_init ( FoobarControlDetailsClass* klass ); +static void foobar_control_details_init ( FoobarControlDetails* self ); +static void foobar_control_details_get_property( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_control_details_set_property( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_control_details_dispose ( GObject* object ); + +G_DEFINE_FINAL_TYPE( FoobarControlDetails, foobar_control_details, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for control details. +// +void foobar_control_details_class_init( FoobarControlDetailsClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + gtk_widget_class_set_css_name( widget_klass, "control-details" ); + gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BIN_LAYOUT ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_control_details_get_property; + object_klass->set_property = foobar_control_details_set_property; + object_klass->dispose = foobar_control_details_dispose; + + props[PROP_CHILD] = g_param_spec_object( + "child", + "Child", + "Child widget for the details revealer.", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE ); + props[PROP_IS_EXPANDED] = g_param_spec_boolean( + "is-expanded", + "Is Expanded", + "Current state of the details revealer.", + FALSE, + G_PARAM_READWRITE ); + props[PROP_INSET_TOP] = g_param_spec_int( + "inset-top", + "Inset Top", + "Inset from the top edge (only visible when expanded).", + 0, + INT_MAX, + 0, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for control details. +// +void foobar_control_details_init( FoobarControlDetails* self ) +{ + GtkWidget* scrolled_window = gtk_scrolled_window_new( ); + gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scrolled_window ), GTK_POLICY_NEVER, GTK_POLICY_EXTERNAL ); + gtk_scrolled_window_set_propagate_natural_height( GTK_SCROLLED_WINDOW( scrolled_window ), TRUE ); + g_object_bind_property( + self, + "child", + scrolled_window, + "child", + G_BINDING_SYNC_CREATE ); + g_object_bind_property( + self, + "inset-top", + scrolled_window, + "margin-top", + G_BINDING_SYNC_CREATE ); + + GtkWidget* revealer = gtk_revealer_new( ); + gtk_revealer_set_transition_type( GTK_REVEALER( revealer ), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN ); + gtk_revealer_set_transition_duration( GTK_REVEALER( revealer ), 100 ); + gtk_revealer_set_child( GTK_REVEALER( revealer ), scrolled_window ); + g_object_bind_property( + self, + "is-expanded", + revealer, + "reveal-child", + G_BINDING_SYNC_CREATE ); + + gtk_widget_set_parent( revealer, GTK_WIDGET( self ) ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_control_details_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarControlDetails* self = (FoobarControlDetails*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + g_value_set_object( value, foobar_control_details_get_child( self ) ); + break; + case PROP_IS_EXPANDED: + g_value_set_boolean( value, foobar_control_details_is_expanded( self ) ); + break; + case PROP_INSET_TOP: + g_value_set_int( value, foobar_control_details_get_inset_top( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Property setter implementation, mapping a property id to a method. +// +void foobar_control_details_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarControlDetails* self = (FoobarControlDetails*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + foobar_control_details_set_child( self, g_value_get_object( value ) ); + break; + case PROP_IS_EXPANDED: + foobar_control_details_set_expanded( self, g_value_get_boolean( value ) ); + break; + case PROP_INSET_TOP: + foobar_control_details_set_inset_top( self, g_value_get_int( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for control details. +// +void foobar_control_details_dispose( GObject* object ) +{ + FoobarControlDetails* self = (FoobarControlDetails*)object; + + GtkWidget* child; + while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) + { + gtk_widget_unparent( child ); + } + + g_clear_object( &self->child ); + + G_OBJECT_CLASS( foobar_control_details_parent_class )->dispose( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new control details instance. +// +GtkWidget* foobar_control_details_new( void ) +{ + return g_object_new( FOOBAR_TYPE_CONTROL_DETAILS, NULL ); +} + +// +// Get the child widget for the details revealer. +// +GtkWidget* foobar_control_details_get_child( FoobarControlDetails* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), NULL ); + return self->child; +} + +// +// Get the current state of the details revealer. +// +gboolean foobar_control_details_is_expanded( FoobarControlDetails* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), FALSE ); + return self->is_expanded; +} + +// +// Get the inset from the top edge (only visible when expanded). +// +gint foobar_control_details_get_inset_top( FoobarControlDetails* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), FALSE ); + return self->inset_top; +} + +// +// Update the child widget for the details revealer. +// +void foobar_control_details_set_child( + FoobarControlDetails* self, + GtkWidget* value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) ); + + if ( self->child != value ) + { + g_clear_object( &self->child ); + if ( value ) { self->child = g_object_ref( value ); } + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] ); + } +} + +// +// Update the current state of the details revealer. +// +void foobar_control_details_set_expanded( + FoobarControlDetails* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) ); + + value = !!value; + if ( self->is_expanded != value ) + { + self->is_expanded = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] ); + + if ( self->is_expanded ) + { + gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" ); + } + else + { + gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" ); + } + } +} + +// +// Update the inset from the top edge (only visible when expanded). +// +void foobar_control_details_set_inset_top( + FoobarControlDetails* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) ); + + value = MAX( value, 0 ); + if ( self->inset_top != value ) + { + self->inset_top = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_TOP] ); + } +} diff --git a/csrc/widgets/control-center/control-details.h b/csrc/widgets/control-center/control-details.h new file mode 100644 index 0000000..ba7b5ec --- /dev/null +++ b/csrc/widgets/control-center/control-details.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_CONTROL_DETAILS foobar_control_details_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarControlDetails, foobar_control_details, FOOBAR, CONTROL_DETAILS, GtkWidget ) + +GtkWidget* foobar_control_details_new ( void ); +GtkWidget* foobar_control_details_get_child ( FoobarControlDetails* self ); +gboolean foobar_control_details_is_expanded ( FoobarControlDetails* self ); +gint foobar_control_details_get_inset_top( FoobarControlDetails* self ); +void foobar_control_details_set_child ( FoobarControlDetails* self, + GtkWidget* value ); +void foobar_control_details_set_expanded ( FoobarControlDetails* self, + gboolean value ); +void foobar_control_details_set_inset_top( FoobarControlDetails* self, + gint value ); + +G_END_DECLS diff --git a/csrc/widgets/control-center/control-slider.c b/csrc/widgets/control-center/control-slider.c new file mode 100644 index 0000000..9c69190 --- /dev/null +++ b/csrc/widgets/control-center/control-slider.c @@ -0,0 +1,428 @@ +#include "widgets/control-center/control-slider.h" + +// +// FoobarControlSlider: +// +// A slider in the "controls" section of the control center with a title and icon, as well as an optional secondary +// button to toggle a details view. +// + +struct _FoobarControlSlider +{ + GtkWidget parent_instance; + gchar* icon_name; + gchar* label; + gboolean can_expand; + gboolean is_expanded; + gint percentage; +}; + +enum +{ + PROP_ICON_NAME = 1, + PROP_LABEL, + PROP_CAN_EXPAND, + PROP_IS_EXPANDED, + PROP_PERCENTAGE, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_control_slider_class_init ( FoobarControlSliderClass* klass ); +static void foobar_control_slider_init ( FoobarControlSlider* self ); +static void foobar_control_slider_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_control_slider_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_control_slider_dispose ( GObject* object ); +static void foobar_control_slider_finalize ( GObject* object ); +static void foobar_control_slider_handle_expand_clicked( GtkButton* button, + gpointer userdata ); + +G_DEFINE_FINAL_TYPE( FoobarControlSlider, foobar_control_slider, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for control sliders. +// +void foobar_control_slider_class_init( FoobarControlSliderClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + gtk_widget_class_set_css_name( widget_klass, "control-slider" ); + gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_control_slider_get_property; + object_klass->set_property = foobar_control_slider_set_property; + object_klass->dispose = foobar_control_slider_dispose; + object_klass->finalize = foobar_control_slider_finalize; + + props[PROP_ICON_NAME] = g_param_spec_string( + "icon-name", + "Icon Name", + "Name of the icon to be displayed next to the label.", + NULL, + G_PARAM_READWRITE ); + props[PROP_LABEL] = g_param_spec_string( + "label", + "Label", + "The slider's main label.", + NULL, + G_PARAM_READWRITE ); + props[PROP_CAN_EXPAND] = g_param_spec_boolean( + "can-expand", + "Can Expand", + "Indicates whether some details are hidden and can be revealed by a toggle button next to the slider.", + FALSE, + G_PARAM_READWRITE ); + props[PROP_IS_EXPANDED] = g_param_spec_boolean( + "is-expanded", + "Is Expanded", + "Indicates whether additional details should be shown.", + FALSE, + G_PARAM_READWRITE ); + props[PROP_PERCENTAGE] = g_param_spec_int( + "percentage", + "Percentage", + "Value shown by the slider.", + 0, + 100, + 0, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for control sliders. +// +void foobar_control_slider_init( FoobarControlSlider* self ) +{ + GtkWidget* icon = gtk_image_new( ); + gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); + g_object_bind_property( + self, + "icon-name", + icon, + "icon-name", + G_BINDING_SYNC_CREATE ); + + GtkWidget* label = gtk_label_new( NULL ); + gtk_widget_set_valign( label, GTK_ALIGN_CENTER ); + gtk_widget_set_hexpand( label, TRUE ); + gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); + gtk_label_set_xalign( GTK_LABEL( label ), 0 ); + g_object_bind_property( + self, + "label", + label, + "label", + G_BINDING_SYNC_CREATE ); + + GtkWidget* header = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); + gtk_box_append( GTK_BOX( header ), icon ); + gtk_box_append( GTK_BOX( header ), label ); + + GtkWidget* scale = gtk_scale_new_with_range( GTK_ORIENTATION_HORIZONTAL, 0, 100, 2 ); + g_object_bind_property( + self, + "percentage", + gtk_range_get_adjustment( GTK_RANGE( scale ) ), + "value", + G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL ); + + GtkWidget* primary_content = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); + gtk_widget_add_css_class( primary_content, "primary" ); + gtk_box_append( GTK_BOX( primary_content ), header ); + gtk_box_append( GTK_BOX( primary_content ), scale ); + + GtkWidget* expand_button = gtk_button_new_from_icon_name( "fluent-chevron-right-symbolic" ); + gtk_widget_add_css_class( expand_button, "expand" ); + g_object_bind_property( + self, + "can-expand", + expand_button, + "visible", + G_BINDING_SYNC_CREATE ); + g_signal_connect( expand_button, "clicked", G_CALLBACK( foobar_control_slider_handle_expand_clicked ), self ); + + GtkLayoutManager* layout = gtk_widget_get_layout_manager( GTK_WIDGET( self ) ); + gtk_orientable_set_orientation( GTK_ORIENTABLE( layout ), GTK_ORIENTATION_HORIZONTAL ); + gtk_widget_insert_before( primary_content, GTK_WIDGET( self ), NULL ); + gtk_widget_insert_before( expand_button, GTK_WIDGET( self ), NULL ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_control_slider_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarControlSlider* self = (FoobarControlSlider*)object; + + switch ( prop_id ) + { + case PROP_ICON_NAME: + g_value_set_string( value, foobar_control_slider_get_icon_name( self ) ); + break; + case PROP_LABEL: + g_value_set_string( value, foobar_control_slider_get_label( self ) ); + break; + case PROP_CAN_EXPAND: + g_value_set_boolean( value, foobar_control_slider_can_expand( self ) ); + break; + case PROP_IS_EXPANDED: + g_value_set_boolean( value, foobar_control_slider_is_expanded( self ) ); + break; + case PROP_PERCENTAGE: + g_value_set_int( value, foobar_control_slider_get_percentage( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Property setter implementation, mapping a property id to a method. +// +void foobar_control_slider_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarControlSlider* self = (FoobarControlSlider*)object; + + switch ( prop_id ) + { + case PROP_ICON_NAME: + foobar_control_slider_set_icon_name( self, g_value_get_string( value ) ); + break; + case PROP_LABEL: + foobar_control_slider_set_label( self, g_value_get_string( value ) ); + break; + case PROP_CAN_EXPAND: + foobar_control_slider_set_can_expand( self, g_value_get_boolean( value ) ); + break; + case PROP_IS_EXPANDED: + foobar_control_slider_set_expanded( self, g_value_get_boolean( value ) ); + break; + case PROP_PERCENTAGE: + foobar_control_slider_set_percentage( self, g_value_get_int( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for control sliders. +// +void foobar_control_slider_dispose( GObject* object ) +{ + FoobarControlSlider* self = (FoobarControlSlider*)object; + + GtkWidget* child; + while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) + { + gtk_widget_unparent( child ); + } + + G_OBJECT_CLASS( foobar_control_slider_parent_class )->dispose( object ); +} + +// +// Instance cleanup for control sliders. +// +void foobar_control_slider_finalize( GObject* object ) +{ + FoobarControlSlider* self = (FoobarControlSlider*)object; + + g_clear_pointer( &self->icon_name, g_free ); + g_clear_pointer( &self->label, g_free ); + + G_OBJECT_CLASS( foobar_control_slider_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new control slider instance. +// +GtkWidget* foobar_control_slider_new( void ) +{ + return g_object_new( FOOBAR_TYPE_CONTROL_SLIDER, NULL ); +} + +// +// Get the name of the icon to be displayed next to the label. +// +gchar const* foobar_control_slider_get_icon_name( FoobarControlSlider* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), NULL ); + return self->icon_name; +} + +// +// Get the slider's main label. +// +gchar const* foobar_control_slider_get_label( FoobarControlSlider* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), NULL ); + return self->label; +} + +// +// Check whether some details are hidden and can be revealed by a toggle button next to the slider. +// +gboolean foobar_control_slider_can_expand( FoobarControlSlider* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE ); + return self->can_expand; +} + +// +// Check whether additional details should currently be shown. +// +gboolean foobar_control_slider_is_expanded( FoobarControlSlider* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE ); + return self->is_expanded; +} + +// +// Get the value shown by the slider. +// +gint foobar_control_slider_get_percentage( FoobarControlSlider* self ) +{ + g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE ); + return self->percentage; +} + +// +// Update the name of the icon to be displayed next to the label. +// +void foobar_control_slider_set_icon_name( + FoobarControlSlider* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); + + if ( g_strcmp0( self->icon_name, value ) ) + { + g_clear_pointer( &self->icon_name, g_free ); + self->icon_name = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ICON_NAME] ); + } +} + +// +// Update the slider's main label. +// +void foobar_control_slider_set_label( + FoobarControlSlider* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); + + if ( g_strcmp0( self->label, value ) ) + { + g_clear_pointer( &self->label, g_free ); + self->label = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] ); + } +} + +// +// Update whether some details are hidden and can be revealed by a toggle button next to the slider. +// +void foobar_control_slider_set_can_expand( + FoobarControlSlider* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); + + if ( !value ) { foobar_control_slider_set_expanded( self, FALSE ); } + + value = !!value; + if ( self->can_expand != value ) + { + self->can_expand = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_EXPAND] ); + } +} + +// +// Update whether additional details are currently shown. +// +void foobar_control_slider_set_expanded( + FoobarControlSlider* self, + gboolean value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); + + value = value && self->can_expand; + if ( self->is_expanded != value ) + { + self->is_expanded = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] ); + + if ( self->is_expanded ) + { + gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" ); + } + else + { + gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" ); + } + } +} + +// +// Update the value shown by the slider. +// +void foobar_control_slider_set_percentage( FoobarControlSlider* self, gint value ) +{ + g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); + + value = CLAMP( value, 0, 100 ); + if ( self->percentage != value ) + { + self->percentage = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_PERCENTAGE] ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called when the expand button was clicked. +// +// If allowed, this will toggle the "expanded" state of the slider. +// +void foobar_control_slider_handle_expand_clicked( GtkButton* button, gpointer userdata ) +{ + (void)button; + FoobarControlSlider* self = (FoobarControlSlider*)userdata; + + if ( foobar_control_slider_can_expand( self ) ) + { + foobar_control_slider_set_expanded( self, !foobar_control_slider_is_expanded( self ) ); + } +} diff --git a/csrc/widgets/control-center/control-slider.h b/csrc/widgets/control-center/control-slider.h new file mode 100644 index 0000000..f797b5c --- /dev/null +++ b/csrc/widgets/control-center/control-slider.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_CONTROL_SLIDER foobar_control_slider_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarControlSlider, foobar_control_slider, FOOBAR, CONTROL_SLIDER, GtkWidget ) + +GtkWidget* foobar_control_slider_new ( void ); +gchar const* foobar_control_slider_get_icon_name ( FoobarControlSlider* self ); +gchar const* foobar_control_slider_get_label ( FoobarControlSlider* self ); +gboolean foobar_control_slider_can_expand ( FoobarControlSlider* self ); +gboolean foobar_control_slider_is_expanded ( FoobarControlSlider* self ); +gint foobar_control_slider_get_percentage( FoobarControlSlider* self ); +void foobar_control_slider_set_icon_name ( FoobarControlSlider* self, + gchar const* value ); +void foobar_control_slider_set_label ( FoobarControlSlider* self, + gchar const* value ); +void foobar_control_slider_set_can_expand( FoobarControlSlider* self, + gboolean value ); +void foobar_control_slider_set_expanded ( FoobarControlSlider* self, + gboolean value ); +void foobar_control_slider_set_percentage( FoobarControlSlider* self, + gint value ); + +G_END_DECLS diff --git a/csrc/widgets/control-center/meson.build b/csrc/widgets/control-center/meson.build new file mode 100644 index 0000000..2cdc9ca --- /dev/null +++ b/csrc/widgets/control-center/meson.build @@ -0,0 +1,6 @@ +foobar_sources += files( + 'control-button.c', + 'control-slider.c', + 'control-details.c', + 'control-details-item.c', +) diff --git a/csrc/widgets/inset-container.c b/csrc/widgets/inset-container.c new file mode 100644 index 0000000..052f872 --- /dev/null +++ b/csrc/widgets/inset-container.c @@ -0,0 +1,363 @@ +#include "widgets/inset-container.h" + +// +// FoobarInsetContainer: +// +// A custom widget which renders a child with an inset/padding. +// +// Notably, it also supports negative inset values which is useful in order to provide uniform spacing between widgets. +// + +struct _FoobarInsetContainer +{ + GtkWidget parent_instance; + GtkWidget* child; + gint inset_horizontal; + gint inset_vertical; +}; + +enum +{ + PROP_CHILD = 1, + PROP_INSET_HORIZONTAL, + PROP_INSET_VERTICAL, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_inset_container_class_init ( FoobarInsetContainerClass* klass ); +static void foobar_inset_container_init ( FoobarInsetContainer* self ); +static void foobar_inset_container_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_inset_container_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_inset_container_dispose ( GObject* object ); +static GtkSizeRequestMode foobar_inset_container_get_request_mode( GtkWidget* widget ); +static void foobar_inset_container_measure ( GtkWidget* widget, + GtkOrientation orientation, + int for_size, + int* minimum, + int* natural, + int* minimum_baseline, + int* natural_baseline ); +static void foobar_inset_container_size_allocate ( GtkWidget* widget, + int width, + int height, + int baseline ); + +G_DEFINE_FINAL_TYPE( FoobarInsetContainer, foobar_inset_container, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for inset containers. +// +void foobar_inset_container_class_init( FoobarInsetContainerClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + widget_klass->get_request_mode = foobar_inset_container_get_request_mode; + widget_klass->measure = foobar_inset_container_measure; + widget_klass->size_allocate = foobar_inset_container_size_allocate; + gtk_widget_class_set_css_name( widget_klass, "inset-container" ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_inset_container_get_property; + object_klass->set_property = foobar_inset_container_set_property; + object_klass->dispose = foobar_inset_container_dispose; + + props[PROP_CHILD] = g_param_spec_object( + "child", + "Child", + "Child widget for the panel item.", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE ); + props[PROP_INSET_HORIZONTAL] = g_param_spec_int( + "inset-horizontal", + "Inset Horizontal", + "The horizontal offset (may be negative).", + INT_MIN, + INT_MAX, + 0, + G_PARAM_READWRITE ); + props[PROP_INSET_VERTICAL] = g_param_spec_int( + "inset-vertical", + "Inset Vertical", + "The vertical offset (may be negative).", + INT_MIN, + INT_MAX, + 0, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for inset containers. +// +void foobar_inset_container_init( FoobarInsetContainer* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_inset_container_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarInsetContainer* self = (FoobarInsetContainer*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + g_value_set_object( value, foobar_inset_container_get_child( self ) ); + break; + case PROP_INSET_HORIZONTAL: + g_value_set_int( value, foobar_inset_container_get_inset_horizontal( self ) ); + break; + case PROP_INSET_VERTICAL: + g_value_set_int( value, foobar_inset_container_get_inset_vertical( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Property setter implementation, mapping a property id to a method. +// +void foobar_inset_container_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarInsetContainer* self = (FoobarInsetContainer*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + foobar_inset_container_set_child( self, g_value_get_object( value ) ); + break; + case PROP_INSET_HORIZONTAL: + foobar_inset_container_set_inset_horizontal( self, g_value_get_int( value ) ); + break; + case PROP_INSET_VERTICAL: + foobar_inset_container_set_inset_vertical( self, g_value_get_int( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for inset containers. +// +void foobar_inset_container_dispose( GObject* object ) +{ + FoobarInsetContainer* self = (FoobarInsetContainer*)object; + + g_clear_pointer( &self->child, gtk_widget_unparent ); + + G_OBJECT_CLASS( foobar_inset_container_parent_class )->dispose( object ); +} + +// +// Determine the size request mode used to measure the widget. +// +// This implementation just forwards the request mode from the child widget. +// +GtkSizeRequestMode foobar_inset_container_get_request_mode( GtkWidget* widget ) +{ + FoobarInsetContainer* self = (FoobarInsetContainer*)widget; + if ( !self->child ) { return GTK_SIZE_REQUEST_CONSTANT_SIZE; } + + return gtk_widget_get_request_mode( self->child ); +} + +// +// Compute the requested size of the widget. +// +// This implementation uses the size requested by the child widget and then adds the inset to it. +// +void foobar_inset_container_measure( + GtkWidget* widget, + GtkOrientation orientation, + int for_size, + int* minimum, + int* natural, + int* minimum_baseline, + int* natural_baseline ) +{ + FoobarInsetContainer* self = (FoobarInsetContainer*)widget; + if ( !self->child ) + { + *minimum = 0; + *natural = 0; + *minimum_baseline = 0; + *natural_baseline = 0; + return; + } + + // Get the inset for the given orientation and the other one. + + gint inset; + gint other_inset; + switch ( orientation ) + { + case GTK_ORIENTATION_HORIZONTAL: + inset = self->inset_horizontal; + other_inset = self->inset_vertical; + break; + case GTK_ORIENTATION_VERTICAL: + inset = self->inset_vertical; + other_inset = self->inset_horizontal; + break; + default: + g_warn_if_reached( ); + return; + } + + // Compute the new size, taking into consideration that the inset might have already been added to measure the other + // side and needs to be subtracted before passing it to the child widget's measure function. + + for_size = MAX( 0, for_size - ( 2 * other_inset ) ); + gtk_widget_measure( self->child, orientation, for_size, minimum, natural, minimum_baseline, natural_baseline ); + *minimum = MAX( 0, *minimum + ( 2 * inset ) ); + *natural = MAX( 0, *natural + ( 2 * inset ) ); + if ( orientation == GTK_ORIENTATION_VERTICAL ) + { + *minimum_baseline = MAX( 0, *minimum_baseline + inset ); + *natural_baseline = MAX( 0, *natural_baseline + inset ); + } +} + +// +// Arrange the child widget. +// +void foobar_inset_container_size_allocate( + GtkWidget* widget, + int width, + int height, + int baseline ) +{ + FoobarInsetContainer* self = (FoobarInsetContainer*)widget; + if ( !self->child ) { return; } + + GtkAllocation allocation = + { + .width = MAX( 0, width - 2 * self->inset_horizontal ), + .height = MAX( 0, height - 2 * self->inset_vertical ), + .x = self->inset_horizontal, + .y = self->inset_vertical, + }; + gtk_widget_size_allocate( self->child, &allocation, MAX( 0, baseline + self->inset_vertical ) ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new inset container instance. +// +GtkWidget* foobar_inset_container_new( void ) +{ + return g_object_new( FOOBAR_TYPE_INSET_CONTAINER, NULL ); +} + +// +// Get the container's child widget. +// +GtkWidget* foobar_inset_container_get_child( FoobarInsetContainer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_INSET_CONTAINER( self ), NULL ); + return self->child; +} + +// +// Get the inset applied to the left and right side of the widget. +// +gint foobar_inset_container_get_inset_horizontal( FoobarInsetContainer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_INSET_CONTAINER( self ), 0 ); + return self->inset_horizontal; +} + +// +// Get the inset applied to the top and bottom side of the widget. +// +gint foobar_inset_container_get_inset_vertical( FoobarInsetContainer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_INSET_CONTAINER( self ), 0 ); + return self->inset_vertical; +} + +// +// Update the container's child widget. +// +void foobar_inset_container_set_child( + FoobarInsetContainer* self, + GtkWidget* child ) +{ + g_return_if_fail( FOOBAR_IS_INSET_CONTAINER( self ) ); + g_return_if_fail( child == NULL || self->child == child || gtk_widget_get_parent( child ) == NULL ); + + if ( self->child != child ) + { + g_clear_pointer( &self->child, gtk_widget_unparent ); + + if ( child ) + { + self->child = child; + gtk_widget_set_parent( child, GTK_WIDGET( self ) ); + } + + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] ); + } +} + +// +// Update the inset applied to the left and right side of the widget. +// +void foobar_inset_container_set_inset_horizontal( + FoobarInsetContainer* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_INSET_CONTAINER( self ) ); + + if ( self->inset_horizontal != value ) + { + self->inset_horizontal = value; + gtk_widget_queue_resize( GTK_WIDGET( self ) ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_HORIZONTAL] ); + } +} + +// +// Update the inset applied to the top and bottom side of the widget. +// +void foobar_inset_container_set_inset_vertical( + FoobarInsetContainer* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_INSET_CONTAINER( self ) ); + + if ( self->inset_vertical != value ) + { + self->inset_vertical = value; + gtk_widget_queue_resize( GTK_WIDGET( self ) ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_VERTICAL] ); + } +} diff --git a/csrc/widgets/inset-container.h b/csrc/widgets/inset-container.h new file mode 100644 index 0000000..316d2be --- /dev/null +++ b/csrc/widgets/inset-container.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_INSET_CONTAINER foobar_inset_container_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarInsetContainer, foobar_inset_container, FOOBAR, INSET_CONTAINER, GtkWidget ) + +GtkWidget* foobar_inset_container_new ( void ); +GtkWidget* foobar_inset_container_get_child ( FoobarInsetContainer* self ); +gint foobar_inset_container_get_inset_horizontal( FoobarInsetContainer* self ); +gint foobar_inset_container_get_inset_vertical ( FoobarInsetContainer* self ); +void foobar_inset_container_set_child ( FoobarInsetContainer* self, + GtkWidget* value ); +void foobar_inset_container_set_inset_horizontal( FoobarInsetContainer* self, + gint value ); +void foobar_inset_container_set_inset_vertical ( FoobarInsetContainer* self, + gint value ); + +G_END_DECLS diff --git a/csrc/widgets/limit-container.c b/csrc/widgets/limit-container.c new file mode 100644 index 0000000..123acef --- /dev/null +++ b/csrc/widgets/limit-container.c @@ -0,0 +1,341 @@ +#include "widgets/limit-container.h" + +// +// FoobarLimitContainer: +// +// A container which uses the natural size requested by the child as its minimum size, but only up to a certain point. +// +// This is useful because gtk_scrolled_window_set_max_content_height doesn't seem to be doing what I understand it to +// do. This way, we can set propagate_natural_height to TRUE and then limit the height through this container. +// + +struct _FoobarLimitContainer +{ + GtkWidget parent_instance; + GtkWidget* child; + gint max_width; + gint max_height; +}; + +enum +{ + PROP_CHILD = 1, + PROP_MAX_WIDTH, + PROP_MAX_HEIGHT, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_limit_container_class_init ( FoobarLimitContainerClass* klass ); +static void foobar_limit_container_init ( FoobarLimitContainer* self ); +static void foobar_limit_container_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_limit_container_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_limit_container_dispose ( GObject* object ); +static GtkSizeRequestMode foobar_limit_container_get_request_mode( GtkWidget* widget ); +static void foobar_limit_container_measure ( GtkWidget* widget, + GtkOrientation orientation, + int for_size, + int* minimum, + int* natural, + int* minimum_baseline, + int* natural_baseline ); +static void foobar_limit_container_size_allocate ( GtkWidget* widget, + int width, + int height, + int baseline ); + +G_DEFINE_FINAL_TYPE( FoobarLimitContainer, foobar_limit_container, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for limit containers. +// +void foobar_limit_container_class_init( FoobarLimitContainerClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + widget_klass->get_request_mode = foobar_limit_container_get_request_mode; + widget_klass->measure = foobar_limit_container_measure; + widget_klass->size_allocate = foobar_limit_container_size_allocate; + gtk_widget_class_set_css_name( widget_klass, "limit-container" ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_limit_container_get_property; + object_klass->set_property = foobar_limit_container_set_property; + object_klass->dispose = foobar_limit_container_dispose; + + props[PROP_CHILD] = g_param_spec_object( + "child", + "Child", + "Child widget for the panel item.", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE ); + props[PROP_MAX_WIDTH] = g_param_spec_int( + "max-width", + "Max Width", + "The maximum natural width supported as a minimum size.", + 0, + INT_MAX, + 0, + G_PARAM_READWRITE ); + props[PROP_MAX_HEIGHT] = g_param_spec_int( + "max-height", + "Max Height", + "The maximum natural height supported as a minimum size.", + 0, + INT_MAX, + 0, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for limit containers. +// +void foobar_limit_container_init( FoobarLimitContainer* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_limit_container_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarLimitContainer* self = (FoobarLimitContainer*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + g_value_set_object( value, foobar_limit_container_get_child( self ) ); + break; + case PROP_MAX_WIDTH: + g_value_set_int( value, foobar_limit_container_get_max_width( self ) ); + break; + case PROP_MAX_HEIGHT: + g_value_set_int( value, foobar_limit_container_get_max_height( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Property setter implementation, mapping a property id to a method. +// +void foobar_limit_container_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarLimitContainer* self = (FoobarLimitContainer*)object; + + switch ( prop_id ) + { + case PROP_CHILD: + foobar_limit_container_set_child( self, g_value_get_object( value ) ); + break; + case PROP_MAX_WIDTH: + foobar_limit_container_set_max_width( self, g_value_get_int( value ) ); + break; + case PROP_MAX_HEIGHT: + foobar_limit_container_set_max_height( self, g_value_get_int( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for limit containers. +// +void foobar_limit_container_dispose( GObject* object ) +{ + FoobarLimitContainer* self = (FoobarLimitContainer*)object; + + g_clear_pointer( &self->child, gtk_widget_unparent ); + + G_OBJECT_CLASS( foobar_limit_container_parent_class )->dispose( object ); +} + +// +// Determine the size request mode used to measure the widget. +// +// This implementation just forwards the request mode from the child widget. +// +GtkSizeRequestMode foobar_limit_container_get_request_mode( GtkWidget* widget ) +{ + FoobarLimitContainer* self = (FoobarLimitContainer*)widget; + if ( !self->child ) { return GTK_SIZE_REQUEST_CONSTANT_SIZE; } + + return gtk_widget_get_request_mode( self->child ); +} + +// +// Compute the requested size of the widget. +// +void foobar_limit_container_measure( + GtkWidget* widget, + GtkOrientation orientation, + int for_size, + int* minimum, + int* natural, + int* minimum_baseline, + int* natural_baseline ) +{ + FoobarLimitContainer* self = (FoobarLimitContainer*)widget; + if ( !self->child ) + { + *minimum = 0; + *natural = 0; + *minimum_baseline = 0; + *natural_baseline = 0; + return; + } + + gtk_widget_measure( self->child, orientation, for_size, minimum, natural, minimum_baseline, natural_baseline ); + gint natural_minimum = *natural; + switch ( orientation ) + { + case GTK_ORIENTATION_HORIZONTAL: + natural_minimum = MIN( natural_minimum, self->max_width ); + break; + case GTK_ORIENTATION_VERTICAL: + natural_minimum = MIN( natural_minimum, self->max_height ); + break; + default: + g_warn_if_reached( ); + break; + } + *minimum = MAX( *minimum, natural_minimum ); +} + +// +// Arrange the child widget. +// +void foobar_limit_container_size_allocate( + GtkWidget* widget, + int width, + int height, + int baseline ) +{ + FoobarLimitContainer* self = (FoobarLimitContainer*)widget; + if ( !self->child ) { return; } + + gtk_widget_allocate( self->child, width, height, baseline, NULL ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new limit container instance. +// +GtkWidget* foobar_limit_container_new( void ) +{ + return g_object_new( FOOBAR_TYPE_LIMIT_CONTAINER, NULL ); +} + +// +// Get the container's child widget. +// +GtkWidget* foobar_limit_container_get_child( FoobarLimitContainer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ), NULL ); + return self->child; +} + +// +// Get the maximum natural width of the child to use as the minimum width for this container. +// +gint foobar_limit_container_get_max_width( FoobarLimitContainer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ), 0 ); + return self->max_width; +} + +// +// Get the maximum natural height of the child to use as the minimum height for this container. +// +gint foobar_limit_container_get_max_height( FoobarLimitContainer* self ) +{ + g_return_val_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ), 0 ); + return self->max_height; +} + +// +// Update the container's child widget. +// +void foobar_limit_container_set_child( + FoobarLimitContainer* self, + GtkWidget* child ) +{ + g_return_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ) ); + g_return_if_fail( child == NULL || self->child == child || gtk_widget_get_parent( child ) == NULL ); + + if ( self->child != child ) + { + g_clear_pointer( &self->child, gtk_widget_unparent ); + + if ( child ) + { + self->child = child; + gtk_widget_set_parent( child, GTK_WIDGET( self ) ); + } + + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] ); + } +} + +// +// Update the maximum natural width of the child to use as the minimum width for this container. +// +void foobar_limit_container_set_max_width( + FoobarLimitContainer* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ) ); + g_return_if_fail( value >= 0 ); + + if ( self->max_width != value ) + { + self->max_width = value; + gtk_widget_queue_resize( GTK_WIDGET( self ) ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_MAX_WIDTH] ); + } +} + +// +// Update the maximum natural height of the child to use as the minimum height for this container. +// +void foobar_limit_container_set_max_height( + FoobarLimitContainer* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ) ); + g_return_if_fail( value >= 0 ); + + if ( self->max_height != value ) + { + self->max_height = value; + gtk_widget_queue_resize( GTK_WIDGET( self ) ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_MAX_HEIGHT] ); + } +} diff --git a/csrc/widgets/limit-container.h b/csrc/widgets/limit-container.h new file mode 100644 index 0000000..cad19aa --- /dev/null +++ b/csrc/widgets/limit-container.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_LIMIT_CONTAINER foobar_limit_container_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarLimitContainer, foobar_limit_container, FOOBAR, LIMIT_CONTAINER, GtkWidget ) + +GtkWidget* foobar_limit_container_new ( void ); +GtkWidget* foobar_limit_container_get_child ( FoobarLimitContainer* self ); +gint foobar_limit_container_get_max_width ( FoobarLimitContainer* self ); +gint foobar_limit_container_get_max_height( FoobarLimitContainer* self ); +void foobar_limit_container_set_child ( FoobarLimitContainer* self, + GtkWidget* value ); +void foobar_limit_container_set_max_width ( FoobarLimitContainer* self, + gint value ); +void foobar_limit_container_set_max_height( FoobarLimitContainer* self, + gint value ); + +G_END_DECLS diff --git a/csrc/widgets/meson.build b/csrc/widgets/meson.build new file mode 100644 index 0000000..7aeb3ca --- /dev/null +++ b/csrc/widgets/meson.build @@ -0,0 +1,8 @@ +foobar_sources += files( + 'inset-container.c', + 'limit-container.c', + 'notification-widget.c', +) + +subdir('control-center') +subdir('panel') diff --git a/csrc/widgets/notification-widget.c b/csrc/widgets/notification-widget.c new file mode 100644 index 0000000..97cfc22 --- /dev/null +++ b/csrc/widgets/notification-widget.c @@ -0,0 +1,821 @@ +#include "widgets/notification-widget.h" + +// +// FoobarNotificationCloseAction: +// +// The action invoked when the user requests to "close" the notification. The notification is either marked as +// "dismissed" and kept around or removed from the list. +// + +G_DEFINE_ENUM_TYPE( + FoobarNotificationCloseAction, + foobar_notification_close_action, + G_DEFINE_ENUM_VALUE( FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE, "remove" ), + G_DEFINE_ENUM_VALUE( FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS, "dismiss" ) ) + +// +// FoobarNotificationWidget: +// +// Shared widget for displaying a notification. The widget also manages an inset/margin -- this is done to allow the +// "close" button to extend into the margin area. +// + +struct _FoobarNotificationWidget +{ + GtkWidget parent_instance; + GtkWidget* container; + GtkWidget* content; + GtkWidget* close_button; + FoobarNotification* notification; + FoobarNotificationCloseAction close_action; + gchar* time_format; + gint min_height; + gint close_button_inset; + gint inset_start; + gint inset_end; + gint inset_top; + gint inset_bottom; +}; + +enum +{ + PROP_NOTIFICATION = 1, + PROP_CLOSE_ACTION, + PROP_TIME_FORMAT, + PROP_MIN_HEIGHT, + PROP_CLOSE_BUTTON_INSET, + PROP_INSET_START, + PROP_INSET_END, + PROP_INSET_TOP, + PROP_INSET_BOTTOM, + N_PROPS, +}; + +static GParamSpec* props[N_PROPS] = { 0 }; + +static void foobar_notification_widget_class_init ( FoobarNotificationWidgetClass* klass ); +static void foobar_notification_widget_init ( FoobarNotificationWidget* self ); +static void foobar_notification_widget_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_notification_widget_set_property ( GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ); +static void foobar_notification_widget_dispose ( GObject* object ); +static void foobar_notification_widget_finalize ( GObject* object ); +static void foobar_notification_widget_handle_enter ( GtkEventControllerMotion* controller, + gdouble x, + gdouble y, + gpointer userdata ); +static void foobar_notification_widget_handle_leave ( GtkEventControllerMotion* controller, + gpointer userdata ); +static void foobar_notification_widget_handle_close_clicked( GtkButton* button, + gpointer userdata ); +static gchar* foobar_notification_widget_compute_time_label ( GtkExpression* expression, + GDateTime* time, + gchar const* format, + gpointer userdata ); +static gboolean foobar_notification_widget_compute_icon_visible( GtkExpression* expression, + GIcon* icon, + gpointer userdata ); +static gboolean foobar_notification_widget_compute_body_visible( GtkExpression* expression, + gchar const* body, + gpointer userdata ); +static void foobar_notification_widget_update_margins ( FoobarNotificationWidget* self ); + +G_DEFINE_FINAL_TYPE( FoobarNotificationWidget, foobar_notification_widget, GTK_TYPE_WIDGET ) + +// --------------------------------------------------------------------------------------------------------------------- +// Widget Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for notifications. +// +void foobar_notification_widget_class_init( FoobarNotificationWidgetClass* klass ) +{ + GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); + gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BIN_LAYOUT ); + gtk_widget_class_set_css_name( widget_klass, "notification" ); + + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_notification_widget_get_property; + object_klass->set_property = foobar_notification_widget_set_property; + object_klass->dispose = foobar_notification_widget_dispose; + object_klass->finalize = foobar_notification_widget_finalize; + + props[PROP_NOTIFICATION] = g_param_spec_object( + "notification", + "Notification", + "Content to display.", + FOOBAR_TYPE_NOTIFICATION, + G_PARAM_READWRITE ); + props[PROP_CLOSE_ACTION] = g_param_spec_enum( + "close-action", + "Close Action", + "Action to execute when the notification is closed.", + FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION, + 0, + G_PARAM_READWRITE ); + props[PROP_TIME_FORMAT] = g_param_spec_string( + "time-format", + "Time Format", + "Time format used to display the notification's timestamp.", + NULL, + G_PARAM_READWRITE ); + props[PROP_MIN_HEIGHT] = g_param_spec_int( + "min-height", + "Minimum Height", + "Minimum requested height for the notification itself.", + 0, + INT_MAX, + 0, + G_PARAM_READWRITE ); + props[PROP_CLOSE_BUTTON_INSET] = g_param_spec_int( + "close-button-inset", + "Close Button Inset", + "Offset of the close button within the notification (may be negative, possibly increasing the actual inset).", + INT_MIN, + INT_MAX, + 0, + G_PARAM_READWRITE ); + props[PROP_INSET_START] = g_param_spec_int( + "inset-start", + "Inset Start", + "Inset for the start edge of the widget.", + 0, + INT_MAX, + 0, + G_PARAM_READWRITE ); + props[PROP_INSET_END] = g_param_spec_int( + "inset-end", + "Inset End", + "Inset for the end edge of the widget.", + 0, + INT_MAX, + 0, + G_PARAM_READWRITE ); + props[PROP_INSET_TOP] = g_param_spec_int( + "inset-top", + "Inset Top", + "Inset for the top edge of the widget.", + 0, + INT_MAX, + 0, + G_PARAM_READWRITE ); + props[PROP_INSET_BOTTOM] = g_param_spec_int( + "inset-bottom", + "Inset Bottom", + "Inset for the bottom edge of the widget.", + 0, + INT_MAX, + 0, + G_PARAM_READWRITE ); + g_object_class_install_properties( object_klass, N_PROPS, props ); +} + +// +// Instance initialization for notifications. +// +void foobar_notification_widget_init( FoobarNotificationWidget* self ) +{ + // Set up the main notification content. + + GtkWidget* icon = gtk_image_new( ); + gtk_widget_add_css_class( icon, "icon" ); + gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); + + GtkWidget* title = gtk_label_new( NULL ); + gtk_label_set_justify( GTK_LABEL( title ), GTK_JUSTIFY_LEFT ); + gtk_label_set_ellipsize( GTK_LABEL( title ), PANGO_ELLIPSIZE_END ); + gtk_label_set_wrap( GTK_LABEL( title ), FALSE ); + gtk_label_set_xalign( GTK_LABEL( title ), 0 ); + gtk_widget_add_css_class( title, "title" ); + gtk_widget_set_valign( title, GTK_ALIGN_BASELINE_CENTER ); + gtk_widget_set_hexpand( title, TRUE ); + + GtkWidget* time = gtk_label_new( NULL ); + gtk_label_set_wrap( GTK_LABEL( time ), FALSE ); + gtk_widget_add_css_class( time, "time" ); + gtk_widget_set_valign( time, GTK_ALIGN_BASELINE_CENTER ); + + GtkWidget* body = gtk_label_new( NULL ); + gtk_label_set_use_markup( GTK_LABEL( body ), TRUE ); + gtk_label_set_justify( GTK_LABEL( body ), GTK_JUSTIFY_LEFT ); + gtk_label_set_ellipsize( GTK_LABEL( body ), PANGO_ELLIPSIZE_END ); + gtk_label_set_wrap( GTK_LABEL( body ), TRUE ); + gtk_label_set_wrap_mode( GTK_LABEL( body ), PANGO_WRAP_WORD_CHAR ); + // XXX: Increase once layout is fixed in GTK (https://gitlab.gnome.org/GNOME/gtk/-/issues/5885) + gtk_label_set_lines( GTK_LABEL( body ), 1 ); + gtk_label_set_xalign( GTK_LABEL( body ), 0 ); + gtk_widget_add_css_class( body, "body" ); + + GtkWidget* header = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); + gtk_box_append( GTK_BOX( header ), title ); + gtk_box_append( GTK_BOX( header ), time ); + + GtkWidget* row = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); + gtk_box_append( GTK_BOX( row ), header ); + gtk_box_append( GTK_BOX( row ), body ); + gtk_widget_set_hexpand( row, TRUE ); + gtk_widget_set_valign( row, GTK_ALIGN_CENTER ); + + self->content = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); + gtk_box_append( GTK_BOX( self->content ), icon ); + gtk_box_append( GTK_BOX( self->content ), row ); + gtk_widget_add_css_class( self->content, "content" ); + + // Set up the overlay for the close button. + + GtkWidget* close_icon = gtk_image_new_from_icon_name( "fluent-dismiss-symbolic" ); + gtk_image_set_pixel_size( GTK_IMAGE( close_icon ), 12 ); + + self->close_button = gtk_button_new( ); + gtk_button_set_child( GTK_BUTTON( self->close_button ), close_icon ); + gtk_widget_add_css_class( self->close_button, "close-button" ); + gtk_widget_set_visible( self->close_button, FALSE ); + gtk_widget_set_halign( self->close_button, GTK_ALIGN_END ); + gtk_widget_set_valign( self->close_button, GTK_ALIGN_START ); + g_signal_connect( self->close_button, "clicked", G_CALLBACK( foobar_notification_widget_handle_close_clicked ), self ); + + GtkEventController* hover_controller = gtk_event_controller_motion_new( ); + g_signal_connect( hover_controller, "enter", G_CALLBACK( foobar_notification_widget_handle_enter ), self ); + g_signal_connect( hover_controller, "leave", G_CALLBACK( foobar_notification_widget_handle_leave ), self ); + + self->container = gtk_overlay_new( ); + gtk_overlay_set_child( GTK_OVERLAY( self->container ), self->content ); + gtk_overlay_add_overlay( GTK_OVERLAY( self->container ), self->close_button ); + gtk_widget_add_controller( self->container, hover_controller ); + gtk_widget_set_parent( self->container, GTK_WIDGET( self ) ); + + // Set up bindings. + + { + GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" ); + GtkExpression* image_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "image" ); + gtk_expression_bind( gtk_expression_ref( image_expr ), icon, "gicon", self ); + GtkExpression* visible_params[] = { image_expr }; + GtkExpression* visible_expr = gtk_cclosure_expression_new( + G_TYPE_BOOLEAN, + NULL, + G_N_ELEMENTS( visible_params ), + visible_params, + G_CALLBACK( foobar_notification_widget_compute_icon_visible ), + NULL, + NULL ); + gtk_expression_bind( visible_expr, icon, "visible", self ); + } + + { + GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" ); + GtkExpression* summary_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "summary" ); + gtk_expression_bind( summary_expr, title, "label", self ); + } + + { + GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" ); + GtkExpression* body_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "body" ); + gtk_expression_bind( gtk_expression_ref( body_expr ), body, "label", self ); + GtkExpression* visible_params[] = { body_expr }; + GtkExpression* visible_expr = gtk_cclosure_expression_new( + G_TYPE_BOOLEAN, + NULL, + G_N_ELEMENTS( visible_params ), + visible_params, + G_CALLBACK( foobar_notification_widget_compute_body_visible ), + NULL, + NULL ); + gtk_expression_bind( visible_expr, body, "visible", self ); + } + + { + GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" ); + GtkExpression* time_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "time" ); + GtkExpression* format_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "time-format" ); + GtkExpression* str_params[] = { time_expr, format_expr }; + GtkExpression* str_expr = gtk_cclosure_expression_new( + G_TYPE_STRING, + NULL, + G_N_ELEMENTS( str_params ), + str_params, + G_CALLBACK( foobar_notification_widget_compute_time_label ), + self, + NULL ); + gtk_expression_bind( str_expr, time, "label", self ); + } +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_notification_widget_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarNotificationWidget* self = (FoobarNotificationWidget*)object; + + switch ( prop_id ) + { + case PROP_NOTIFICATION: + g_value_set_object( value, foobar_notification_widget_get_notification( self ) ); + break; + case PROP_CLOSE_ACTION: + g_value_set_enum( value, foobar_notification_widget_get_close_action( self ) ); + break; + case PROP_TIME_FORMAT: + g_value_set_string( value, foobar_notification_widget_get_time_format( self ) ); + break; + case PROP_MIN_HEIGHT: + g_value_set_int( value, foobar_notification_widget_get_min_height( self ) ); + break; + case PROP_CLOSE_BUTTON_INSET: + g_value_set_int( value, foobar_notification_widget_get_close_button_inset( self ) ); + break; + case PROP_INSET_START: + g_value_set_int( value, foobar_notification_widget_get_inset_start( self ) ); + break; + case PROP_INSET_END: + g_value_set_int( value, foobar_notification_widget_get_inset_end( self ) ); + break; + case PROP_INSET_TOP: + g_value_set_int( value, foobar_notification_widget_get_inset_top( self ) ); + break; + case PROP_INSET_BOTTOM: + g_value_set_int( value, foobar_notification_widget_get_inset_bottom( self ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Property setter implementation, mapping a property id to a method. +// +void foobar_notification_widget_set_property( + GObject* object, + guint prop_id, + GValue const* value, + GParamSpec* pspec ) +{ + FoobarNotificationWidget* self = (FoobarNotificationWidget*)object; + + switch ( prop_id ) + { + case PROP_NOTIFICATION: + foobar_notification_widget_set_notification( self, g_value_get_object( value ) ); + break; + case PROP_CLOSE_ACTION: + foobar_notification_widget_set_close_action( self, g_value_get_enum( value ) ); + break; + case PROP_TIME_FORMAT: + foobar_notification_widget_set_time_format( self, g_value_get_string( value ) ); + break; + case PROP_MIN_HEIGHT: + foobar_notification_widget_set_min_height( self, g_value_get_int( value ) ); + break; + case PROP_CLOSE_BUTTON_INSET: + foobar_notification_widget_set_close_button_inset( self, g_value_get_int( value ) ); + break; + case PROP_INSET_START: + foobar_notification_widget_set_inset_start( self, g_value_get_int( value ) ); + break; + case PROP_INSET_END: + foobar_notification_widget_set_inset_end( self, g_value_get_int( value ) ); + break; + case PROP_INSET_TOP: + foobar_notification_widget_set_inset_top( self, g_value_get_int( value ) ); + break; + case PROP_INSET_BOTTOM: + foobar_notification_widget_set_inset_bottom( self, g_value_get_int( value ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for notifications. +// +void foobar_notification_widget_dispose( GObject* object ) +{ + FoobarNotificationWidget* self = (FoobarNotificationWidget*)object; + + GtkWidget* child; + while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) + { + gtk_widget_unparent( child ); + } + + G_OBJECT_CLASS( foobar_notification_widget_parent_class )->dispose( object ); +} + +// +// Instance cleanup for notifications. +// +void foobar_notification_widget_finalize( GObject* object ) +{ + FoobarNotificationWidget* self = (FoobarNotificationWidget*)object; + + g_clear_object( &self->notification ); + g_clear_pointer( &self->time_format, g_free ); + + G_OBJECT_CLASS( foobar_notification_widget_parent_class )->finalize( object ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new notification widget instance. +// +GtkWidget* foobar_notification_widget_new( void ) +{ + return g_object_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL ); +} + +// +// Get the notification model currently displayed. +// +FoobarNotification* foobar_notification_widget_get_notification( FoobarNotificationWidget* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), NULL ); + return self->notification; +} + +// +// Get the action invoked when the user wants to "close" a notification. +// +FoobarNotificationCloseAction foobar_notification_widget_get_close_action( FoobarNotificationWidget* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); + return self->close_action; +} + +// +// Get the time format string used to display the notification's timestamp. +// +gchar const* foobar_notification_widget_get_time_format( FoobarNotificationWidget* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), NULL ); + return self->time_format; +} + +// +// Get the minimum height of the notification (excluding the inset). +// +gint foobar_notification_widget_get_min_height( FoobarNotificationWidget* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); + return self->min_height; +} + +// +// Get the offset of the close button within the notification (may be negative, possibly increasing the actual inset). +// +gint foobar_notification_widget_get_close_button_inset( FoobarNotificationWidget* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); + return self->close_button_inset; +} + +// +// Get the margin for the start edge of the notification. +// +gint foobar_notification_widget_get_inset_start( FoobarNotificationWidget* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); + return self->inset_start; +} + +// +// Get the margin for the end edge of the notification. +// +gint foobar_notification_widget_get_inset_end( FoobarNotificationWidget* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); + return self->inset_end; +} + +// +// Get the margin for the top edge of the notification. +// +gint foobar_notification_widget_get_inset_top( FoobarNotificationWidget* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); + return self->inset_top; +} + +// +// Get the margin for the bottom edge of the notification. +// +gint foobar_notification_widget_get_inset_bottom( FoobarNotificationWidget* self ) +{ + g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); + return self->inset_bottom; +} + +// +// Update the notification model currently displayed. +// +void foobar_notification_widget_set_notification( + FoobarNotificationWidget* self, + FoobarNotification* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); + + if ( self->notification != value ) + { + g_clear_object( &self->notification ); + if ( value ) { self->notification = g_object_ref( value ); } + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_NOTIFICATION] ); + } +} + +// +// Update the action invoked when the user wants to "close" a notification. +// +void foobar_notification_widget_set_close_action( + FoobarNotificationWidget* self, + FoobarNotificationCloseAction value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); + + if ( self->close_action != value ) + { + self->close_action = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CLOSE_ACTION] ); + } +} + +// +// Update the time format string used to display the notification's timestamp. +// +void foobar_notification_widget_set_time_format( + FoobarNotificationWidget* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); + + if ( g_strcmp0( self->time_format, value ) ) + { + g_clear_pointer( &self->time_format, g_free ); + self->time_format = g_strdup( value ); + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_TIME_FORMAT] ); + } +} + +// +// Update the minimum height of the notification (excluding the inset). +// +void foobar_notification_widget_set_min_height( + FoobarNotificationWidget* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); + + value = MAX( value, 0 ); + if ( self->min_height != value ) + { + self->min_height = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_MIN_HEIGHT] ); + gtk_widget_set_size_request( self->content, -1, self->min_height ); + } +} + +// +// Update the offset of the close button within the notification (may be negative, possibly increasing the actual inset). +// +void foobar_notification_widget_set_close_button_inset( + FoobarNotificationWidget* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); + + if ( self->close_button_inset != value ) + { + self->close_button_inset = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CLOSE_BUTTON_INSET] ); + foobar_notification_widget_update_margins( self ); + } +} + +// +// Update the margin for the start edge of the notification. +// +void foobar_notification_widget_set_inset_start( + FoobarNotificationWidget* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); + + value = MAX( value, 0 ); + if ( self->inset_start != value ) + { + self->inset_start = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_START] ); + foobar_notification_widget_update_margins( self ); + } +} + +// +// Update the margin for the end edge of the notification. +// +void foobar_notification_widget_set_inset_end( + FoobarNotificationWidget* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); + + value = MAX( value, 0 ); + if ( self->inset_end != value ) + { + self->inset_end = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_END] ); + foobar_notification_widget_update_margins( self ); + } +} + +// +// Update the margin for the top edge of the notification. +// +void foobar_notification_widget_set_inset_top( + FoobarNotificationWidget* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); + + value = MAX( value, 0 ); + if ( self->inset_top != value ) + { + self->inset_top = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_TOP] ); + foobar_notification_widget_update_margins( self ); + } +} + +// +// Update the margin for the bottom edge of the notification. +// +void foobar_notification_widget_set_inset_bottom( + FoobarNotificationWidget* self, + gint value ) +{ + g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); + + value = MAX( value, 0 ); + if ( self->inset_bottom != value ) + { + self->inset_bottom = value; + g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_BOTTOM] ); + foobar_notification_widget_update_margins( self ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Signal Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Called when the mouse cursor enters the notification. +// +// We will temporarily block the dismissal timeout for the notification and show the close button. +// +void foobar_notification_widget_handle_enter( + GtkEventControllerMotion* controller, + gdouble x, + gdouble y, + gpointer userdata ) +{ + (void)controller; + (void)x; + (void)y; + FoobarNotificationWidget* self = (FoobarNotificationWidget*)userdata; + + gtk_widget_set_visible( self->close_button, TRUE ); + if ( self->notification ) { foobar_notification_block_timeout( self->notification ); } +} + +// +// Called when the mouse cursor leaves the notification. +// +// We will unblock the dismissal timeout for the notification and hide the close button. +// +void foobar_notification_widget_handle_leave( + GtkEventControllerMotion* controller, + gpointer userdata ) +{ + (void)controller; + FoobarNotificationWidget* self = (FoobarNotificationWidget*)userdata; + + gtk_widget_set_visible( self->close_button, FALSE ); + if ( self->notification ) { foobar_notification_resume_timeout( self->notification ); } +} + +// +// Called when the user has clicked the "close" button. +// +// This either marks the notification as dismissed or removes it. +// +void foobar_notification_widget_handle_close_clicked( + GtkButton* button, + gpointer userdata ) +{ + (void)button; + FoobarNotificationWidget* self = (FoobarNotificationWidget*)userdata; + + if ( self->notification ) + { + switch ( self->close_action ) + { + case FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE: + foobar_notification_close( self->notification ); + break; + case FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS: + foobar_notification_dismiss( self->notification ); + break; + default: + g_warn_if_reached( ); + break; + } + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Value Converters +// --------------------------------------------------------------------------------------------------------------------- + +// +// Derive the timestamp label value from the notification's timestamp and the selected time format. +// +gchar* foobar_notification_widget_compute_time_label( + GtkExpression* expression, + GDateTime* time, + gchar const* format, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + return format != NULL ? g_date_time_format( time, format ) : NULL; +} + +// +// Derive the visibility of the notification icon from its value. +// +gboolean foobar_notification_widget_compute_icon_visible( + GtkExpression* expression, + GIcon* icon, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + return icon != NULL; +} + +// +// Derive the visibility of the notification body label from its value. +// +gboolean foobar_notification_widget_compute_body_visible( + GtkExpression* expression, + gchar const* body, + gpointer userdata ) +{ + (void)expression; + (void)userdata; + + return body != NULL && g_strcmp0( body, "" ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Update the margins of the content widget and close button to the current inset values. +// +void foobar_notification_widget_update_margins( FoobarNotificationWidget* self ) +{ + gint inset = MAX( self->close_button_inset, 0 ); + gint overrun = MAX( -self->close_button_inset, 0 ); + gint actual_inset_end = MAX( self->inset_end, overrun ); + gint actual_inset_top = MAX( self->inset_top, overrun ); + + gtk_widget_set_margin_start( self->container, self->inset_start ); + gtk_widget_set_margin_end( self->container, actual_inset_end - overrun ); + gtk_widget_set_margin_top( self->container, actual_inset_top - overrun ); + gtk_widget_set_margin_bottom( self->container, self->inset_bottom ); + + gtk_widget_set_margin_end( self->content, overrun ); + gtk_widget_set_margin_top( self->content, overrun ); + + gtk_widget_set_margin_end( self->close_button, inset ); + gtk_widget_set_margin_top( self->close_button, inset ); +} diff --git a/csrc/widgets/notification-widget.h b/csrc/widgets/notification-widget.h new file mode 100644 index 0000000..af9de49 --- /dev/null +++ b/csrc/widgets/notification-widget.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include "services/notification-service.h" + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION foobar_notification_close_action_get_type( ) +#define FOOBAR_TYPE_NOTIFICATION_WIDGET foobar_notification_widget_get_type( ) + +typedef enum +{ + FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE, + FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS, +} FoobarNotificationCloseAction; + +GType foobar_notification_close_action_get_type( void ); + +G_DECLARE_FINAL_TYPE( FoobarNotificationWidget, foobar_notification_widget, FOOBAR, NOTIFICATION_WIDGET, GtkWidget ) + +GtkWidget* foobar_notification_widget_new ( void ); +FoobarNotification* foobar_notification_widget_get_notification ( FoobarNotificationWidget* self ); +FoobarNotificationCloseAction foobar_notification_widget_get_close_action ( FoobarNotificationWidget* self ); +gchar const* foobar_notification_widget_get_time_format ( FoobarNotificationWidget* self ); +gint foobar_notification_widget_get_min_height ( FoobarNotificationWidget* self ); +gint foobar_notification_widget_get_close_button_inset( FoobarNotificationWidget* self ); +gint foobar_notification_widget_get_inset_start ( FoobarNotificationWidget* self ); +gint foobar_notification_widget_get_inset_end ( FoobarNotificationWidget* self ); +gint foobar_notification_widget_get_inset_top ( FoobarNotificationWidget* self ); +gint foobar_notification_widget_get_inset_bottom ( FoobarNotificationWidget* self ); +void foobar_notification_widget_set_notification ( FoobarNotificationWidget* self, + FoobarNotification* value ); +void foobar_notification_widget_set_close_action ( FoobarNotificationWidget* self, + FoobarNotificationCloseAction value ); +void foobar_notification_widget_set_time_format ( FoobarNotificationWidget* self, + gchar const* value ); +void foobar_notification_widget_set_min_height ( FoobarNotificationWidget* self, + gint value ); +void foobar_notification_widget_set_close_button_inset( FoobarNotificationWidget* self, + gint value ); +void foobar_notification_widget_set_inset_start ( FoobarNotificationWidget* self, + gint value ); +void foobar_notification_widget_set_inset_end ( FoobarNotificationWidget* self, + gint value ); +void foobar_notification_widget_set_inset_top ( FoobarNotificationWidget* self, + gint value ); +void foobar_notification_widget_set_inset_bottom ( FoobarNotificationWidget* self, + gint value ); + +G_END_DECLS diff --git a/csrc/widgets/panel/meson.build b/csrc/widgets/panel/meson.build new file mode 100644 index 0000000..10afc94 --- /dev/null +++ b/csrc/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', +) diff --git a/csrc/widgets/panel/panel-item-clock.c b/csrc/widgets/panel/panel-item-clock.c new file mode 100644 index 0000000..00b2cc5 --- /dev/null +++ b/csrc/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 ); +} diff --git a/csrc/widgets/panel/panel-item-clock.h b/csrc/widgets/panel/panel-item-clock.h new file mode 100644 index 0000000..5ff7e5b --- /dev/null +++ b/csrc/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 diff --git a/csrc/widgets/panel/panel-item-icon.c b/csrc/widgets/panel/panel-item-icon.c new file mode 100644 index 0000000..0a3b4e9 --- /dev/null +++ b/csrc/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 ); +} diff --git a/csrc/widgets/panel/panel-item-icon.h b/csrc/widgets/panel/panel-item-icon.h new file mode 100644 index 0000000..2b4ac1f --- /dev/null +++ b/csrc/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 diff --git a/csrc/widgets/panel/panel-item-status.c b/csrc/widgets/panel/panel-item-status.c new file mode 100644 index 0000000..f06bb68 --- /dev/null +++ b/csrc/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 ); + } +} diff --git a/csrc/widgets/panel/panel-item-status.h b/csrc/widgets/panel/panel-item-status.h new file mode 100644 index 0000000..0f1cfdb --- /dev/null +++ b/csrc/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 diff --git a/csrc/widgets/panel/panel-item-workspaces.c b/csrc/widgets/panel/panel-item-workspaces.c new file mode 100644 index 0000000..dff629b --- /dev/null +++ b/csrc/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 ); +} diff --git a/csrc/widgets/panel/panel-item-workspaces.h b/csrc/widgets/panel/panel-item-workspaces.h new file mode 100644 index 0000000..d179217 --- /dev/null +++ b/csrc/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 diff --git a/csrc/widgets/panel/panel-item.c b/csrc/widgets/panel/panel-item.c new file mode 100644 index 0000000..918d9f5 --- /dev/null +++ b/csrc/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; + } +} diff --git a/csrc/widgets/panel/panel-item.h b/csrc/widgets/panel/panel-item.h new file mode 100644 index 0000000..12a7e35 --- /dev/null +++ b/csrc/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 diff --git a/flake.lock b/flake.lock index 1ccee2c..7b2d254 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1726560853, - "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1729880355, - "narHash": "sha256-RP+OQ6koQQLX5nw0NmcDrzvGL8HDLnyXt/jHhL1jwjM=", + "lastModified": 1750134718, + "narHash": "sha256-v263g4GbxXv87hMXMCpjkIxd/viIF7p3JpJrwgKdNiI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "18536bf04cd71abd345f9579158841376fdd0c5a", + "rev": "9e83b64f727c88a7711a2c463a7b16eedb69a84c", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index b63d871..a094789 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,13 @@ flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; + haskell = pkgs.haskellPackages.ghcWithPackages (p: + with p; [ haskell-language-server ]); + haskell-shell = pkgs.mkShell { + name = "Haskell"; + packages = [ haskell pkgs.gdb pkgs.pkg-config pkgs.cabal-install ]; + hardeningDisable = [ "fortify" ]; + }; foobar = (pkgs.callPackage ./nix/package.nix {}); in { @@ -33,6 +40,7 @@ inputsFrom = [ foobar ]; hardeningDisable = [ "fortify" ]; }; + haskell = haskell-shell; }; }); } diff --git a/foobar.cabal b/foobar.cabal new file mode 100644 index 0000000..d3eea46 --- /dev/null +++ b/foobar.cabal @@ -0,0 +1,15 @@ +cabal-version: 3.0 +name: foobar +version: 2.0.0 +synopsis: A simple bar, launcher, control center, and notification daemon. +build-type: Simple + +common warnings + ghc-options: -Wall + +executable foobar + import: warnings + hs-source-dirs: src + main-is: Main.hs + build-depends: base ^>=4.19.2.0 + default-language: Haskell2010 diff --git a/nix/package.nix b/nix/package.nix index 3b8be8f..e0fcbac 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -12,6 +12,7 @@ pkg-config, gobject-introspection, wayland-scanner, + wayland-protocols, glib, gtk4, json-glib, @@ -28,7 +29,7 @@ dep-gtk4-layer-shell = fetchFromGitHub { owner = "wmww"; repo = "gtk4-layer-shell"; - rev = "v1.0.2"; + rev = "v1.1.1"; hash = "sha256-decjPkFkYy7kIjyozsB7BEmw33wzq1EQyIBrxO36984="; }; dep-gvc = fetchFromGitLab { diff --git a/src/Main.hs b/src/Main.hs new file mode 100644 index 0000000..3b09145 --- /dev/null +++ b/src/Main.hs @@ -0,0 +1,2 @@ +main :: IO () +main = putStrLn "Hello, world!" diff --git a/src/application.c b/src/application.c deleted file mode 100644 index bad62b1..0000000 --- a/src/application.c +++ /dev/null @@ -1,691 +0,0 @@ -#include "application.h" -#include "panel.h" -#include "launcher.h" -#include "control-center.h" -#include "notification-area.h" -#include "dbus/server.h" -#include "services/battery-service.h" -#include "services/clock-service.h" -#include "services/brightness-service.h" -#include "services/quick-answer-service.h" -#include "services/workspace-service.h" -#include "services/notification-service.h" -#include "services/audio-service.h" -#include "services/network-service.h" -#include "services/bluetooth-service.h" -#include "services/application-service.h" -#include "services/configuration-service.h" - -// -// FoobarApplication: -// -// Entry point for the application. This class is responsible for parsing and handling command line arguments, creating -// services and presenting windows as needed. -// -// Only the panel windows are application windows -- when all panels are closed, the application quits. Because of this, -// the application can't own a reference to the panel windows but needs to store their window IDs. Since all other -// windows are not attached to the application object, the application can hold a reference on them. -// - -struct _FoobarApplication -{ - GtkApplication parent_instance; - FoobarBatteryService* battery_service; - FoobarClockService* clock_service; - FoobarBrightnessService* brightness_service; - FoobarWorkspaceService* workspace_service; - FoobarNotificationService* notification_service; - FoobarAudioService* audio_service; - FoobarNetworkService* network_service; - FoobarBluetoothService* bluetooth_service; - FoobarApplicationService* application_service; - FoobarQuickAnswerService* quick_answer_service; - FoobarConfigurationService* configuration_service; - gulong config_handler_id; - FoobarServer* server_skeleton; - GtkCssProvider* style_provider; - guint bus_owner_id; - GArray* panel_window_ids; - gboolean panel_is_multi_monitor; - GListModel* monitors; - gulong monitors_handler_id; - FoobarLauncher* launcher; - FoobarControlCenter* control_center; - FoobarNotificationArea* notification_area; - gboolean option_inspector; - gboolean option_quit; - gboolean option_toggle_launcher; - gboolean option_toggle_control_center; -}; - -static void foobar_application_class_init ( FoobarApplicationClass* klass ); -static void foobar_application_init ( FoobarApplication* self ); -static void foobar_application_activate ( GApplication* app ); -static int foobar_application_command_line ( GApplication* app, - GApplicationCommandLine* cmdline ); -static void foobar_application_finalize ( GObject* object ); -static void foobar_application_handle_bus_acquired ( GDBusConnection* connection, - gchar const* name, - gpointer userdata ); -static gboolean foobar_application_handle_inspector ( FoobarServer* server, - GDBusMethodInvocation* invocation, - gpointer userdata ); -static gboolean foobar_application_handle_quit ( FoobarServer* server, - GDBusMethodInvocation* invocation, - gpointer userdata ); -static gboolean foobar_application_handle_toggle_launcher ( FoobarServer* server, - GDBusMethodInvocation* invocation, - gpointer userdata ); -static gboolean foobar_application_handle_toggle_control_center( FoobarServer* server, - GDBusMethodInvocation* invocation, - gpointer userdata ); -static void foobar_application_handle_config_changed ( GObject* object, - GParamSpec* pspec, - gpointer userdata ); -static void foobar_application_handle_monitors_changed ( GListModel* list, - guint position, - guint removed, - guint added, - gpointer userdata ); -static void foobar_application_destroy_panels ( FoobarApplication* self ); -static void foobar_application_create_panels ( FoobarApplication* self ); -static guint foobar_application_create_panel ( FoobarApplication* self, - GdkMonitor* monitor ); - -G_DEFINE_FINAL_TYPE( FoobarApplication, foobar_application, GTK_TYPE_APPLICATION ) - -// --------------------------------------------------------------------------------------------------------------------- -// Application Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the application. -// -void foobar_application_class_init( FoobarApplicationClass* klass ) -{ - GApplicationClass* app_klass = G_APPLICATION_CLASS( klass ); - app_klass->activate = foobar_application_activate; - app_klass->command_line = foobar_application_command_line; - - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->finalize = foobar_application_finalize; -} - -// -// Instance initialization for the application. -// -void foobar_application_init( FoobarApplication* self ) -{ - g_application_set_default( G_APPLICATION( self ) ); - g_application_set_option_context_summary( G_APPLICATION( self ), "A bar." ); - - GOptionEntry const options[] = - { - { - .long_name = "inspector", - .short_name = 'i', - .flags = G_OPTION_FLAG_NONE, - .arg = G_OPTION_ARG_NONE, - .arg_data = &self->option_inspector, - .description = "Open the GTK inspector for the active bar instance.", - .arg_description = NULL, - }, - { - .long_name = "quit", - .short_name = 'q', - .flags = G_OPTION_FLAG_NONE, - .arg = G_OPTION_ARG_NONE, - .arg_data = &self->option_quit, - .description = "Quit the active bar instance.", - .arg_description = NULL, - }, - { - .long_name = "toggle-launcher", - .short_name = 'l', - .flags = G_OPTION_FLAG_NONE, - .arg = G_OPTION_ARG_NONE, - .arg_data = &self->option_toggle_launcher, - .description = "Toggle the launcher visibility for the active bar instance.", - .arg_description = NULL, - }, - { - .long_name = "toggle-control-center", - .short_name = 'c', - .flags = G_OPTION_FLAG_NONE, - .arg = G_OPTION_ARG_NONE, - .arg_data = &self->option_toggle_control_center, - .description = "Toggle the control center visibility for the active bar instance.", - .arg_description = NULL, - }, - { 0 }, - }; - g_application_add_main_option_entries( G_APPLICATION( self ), options ); -} - -// -// Called by GTK when the application is activated (only for a single instance of the application). -// -// This is where the services are created and the windows are presented. -// -void foobar_application_activate( GApplication* app ) -{ - FoobarApplication* self = (FoobarApplication*)app; - - // Initialize services. - - self->battery_service = foobar_battery_service_new( ); - self->clock_service = foobar_clock_service_new( ); - self->brightness_service = foobar_brightness_service_new( ); - self->workspace_service = foobar_workspace_service_new( ); - self->notification_service = foobar_notification_service_new( ); - self->audio_service = foobar_audio_service_new( ); - self->network_service = foobar_network_service_new( ); - self->bluetooth_service = foobar_bluetooth_service_new( ); - self->application_service = foobar_application_service_new( ); - self->quick_answer_service = foobar_quick_answer_service_new( ); - self->configuration_service = foobar_configuration_service_new( ); - - // Enforce a uniform style by forcing Adwaita and shipping our own icons. - - GtkSettings* settings = gtk_settings_get_default( ); - g_object_set( settings, "gtk-theme-name", "Adwaita", NULL ); - - GtkIconTheme* icon_theme = gtk_icon_theme_get_for_display( gdk_display_get_default( ) ); - gtk_icon_theme_add_resource_path( icon_theme, "/foobar/icons" ); - - // This application instance now becomes the "server" process, listening for commands from clients (which can be - // invoked via the command line). - - self->bus_owner_id = g_bus_own_name( - G_BUS_TYPE_SESSION, - g_application_get_application_id( G_APPLICATION( self ) ), - G_BUS_NAME_OWNER_FLAGS_NONE, - foobar_application_handle_bus_acquired, - NULL, - NULL, - self, - NULL ); - - // React to configuration file changes. - - FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); - foobar_application_apply_configuration( self, foobar_configuration_get_general( config ) ); - self->panel_is_multi_monitor = foobar_panel_configuration_get_multi_monitor( foobar_configuration_get_panel( config ) ); - self->config_handler_id = g_signal_connect( - self->configuration_service, - "notify::current", - G_CALLBACK( foobar_application_handle_config_changed ), - self ); - - // React to monitor configuration changes (for multi-monitor support in the panel). - - self->monitors = gdk_display_get_monitors( gdk_display_get_default( ) ); - g_object_ref( self->monitors ); - - self->monitors_handler_id = g_signal_connect( - self->monitors, - "items-changed", - G_CALLBACK( foobar_application_handle_monitors_changed ), - self ); - - // Create all the windows. - - foobar_application_create_panels( self ); - - self->launcher = foobar_launcher_new( - self->application_service, - self->quick_answer_service, - self->configuration_service ); - g_object_ref( self->launcher ); - - self->control_center = foobar_control_center_new( - self->brightness_service, - self->audio_service, - self->network_service, - self->bluetooth_service, - self->notification_service, - self->configuration_service ); - g_object_ref( self->control_center ); - - self->notification_area = foobar_notification_area_new( - self->notification_service, - self->configuration_service ); - g_object_ref( self->notification_area ); - - // Only present the notification area, all other windows start out as hidden windows. - - gtk_window_present( GTK_WINDOW( self->notification_area ) ); -} - -// -// Called before activating the application, when GTK has parsed the command line arguments. -// -// This method decides whether the application should be activated (if it is the first instance). Otherwise it will -// handle command line flags for controlling the application. -// -int foobar_application_command_line( - GApplication* app, - GApplicationCommandLine* cmdline ) -{ - (void)cmdline; - FoobarApplication* self = (FoobarApplication*)app; - - // Try to open a connection to the existing server. - - g_autoptr( GError ) error = NULL; - g_autoptr( FoobarServer ) proxy = foobar_server_proxy_new_for_bus_sync( - G_BUS_TYPE_SESSION, - G_DBUS_PROXY_FLAGS_NONE, - g_application_get_application_id( G_APPLICATION( self ) ), - "/com/github/hannesschulze/foobar/server", - NULL, - &error ); - - if ( !proxy ) - { - g_printerr( "Unable to create proxy: %s\n", error->message ); - return 1; - } - - // If there is no existing server application, the "name owner" will not be set. - - g_autofree gchar* owner = g_dbus_proxy_get_name_owner( G_DBUS_PROXY( proxy ) ); - if ( owner ) - { - // There is an active instance -> this is a client. Handle all command line flags. - - if ( self->option_inspector && !foobar_server_call_inspector_sync( proxy, NULL, &error ) ) - { - g_printerr( "Unable to open inspector: %s\n", error->message ); - return 2; - } - - if ( self->option_toggle_launcher && !foobar_server_call_toggle_launcher_sync( proxy, NULL, &error ) ) - { - g_printerr( "Unable to toggle launcher: %s\n", error->message ); - return 2; - } - - if ( self->option_toggle_control_center && !foobar_server_call_toggle_control_center_sync( proxy, NULL, &error ) ) - { - g_printerr( "Unable to toggle control center: %s\n", error->message ); - return 2; - } - - if ( self->option_quit && !foobar_server_call_quit_sync( proxy, NULL, &error ) ) - { - g_printerr( "Unable to quit server: %s\n", error->message ); - return 2; - } - } - else if ( !self->option_quit ) - { - // This is the main application -> launch the server. - - g_application_activate( G_APPLICATION( self ) ); - } - - return 0; -} - -// -// Instance cleanup for the application. -// -void foobar_application_finalize( GObject* object ) -{ - FoobarApplication* self = (FoobarApplication*)object; - - // Destroy the DBus server. - - if ( self->server_skeleton ) - { - g_dbus_interface_skeleton_unexport( G_DBUS_INTERFACE_SKELETON( self->server_skeleton ) ); - } - - // Destroy non-application windows. - - if ( self->launcher ) { gtk_window_destroy( GTK_WINDOW( self->launcher ) ); } - if ( self->control_center ) { gtk_window_destroy( GTK_WINDOW( self->control_center ) ); } - if ( self->notification_area ) { gtk_window_destroy( GTK_WINDOW( self->notification_area ) ); } - - // Destroy all other objects. - - g_clear_handle_id( &self->bus_owner_id, g_bus_unown_name ); - g_clear_signal_handler( &self->config_handler_id, self->configuration_service ); - g_clear_signal_handler( &self->monitors_handler_id, self->monitors ); - g_clear_object( &self->battery_service ); - g_clear_object( &self->clock_service ); - g_clear_object( &self->brightness_service ); - g_clear_object( &self->workspace_service ); - g_clear_object( &self->notification_service ); - g_clear_object( &self->audio_service ); - g_clear_object( &self->network_service ); - g_clear_object( &self->bluetooth_service ); - g_clear_object( &self->application_service ); - g_clear_object( &self->quick_answer_service ); - g_clear_object( &self->configuration_service ); - g_clear_object( &self->style_provider ); - g_clear_object( &self->server_skeleton ); - g_clear_object( &self->launcher ); - g_clear_object( &self->control_center ); - g_clear_object( &self->notification_area ); - g_clear_object( &self->monitors ); - g_clear_pointer( &self->panel_window_ids, g_array_unref ); - - G_OBJECT_CLASS( foobar_application_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new application instance. -// -FoobarApplication* foobar_application_new( void ) -{ - return g_object_new( - FOOBAR_TYPE_APPLICATION, - "application-id", - "com.github.hannesschulze.foobar", - "flags", - G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_NON_UNIQUE, - NULL ); -} - -// -// Toggle the launcher window's visibility. -// -void foobar_application_toggle_launcher( FoobarApplication* self ) -{ - g_return_if_fail( FOOBAR_IS_APPLICATION( self ) ); - - gboolean previously_visible = gtk_widget_is_visible( GTK_WIDGET( self->launcher ) ); - gtk_widget_set_visible( GTK_WIDGET( self->launcher ), !previously_visible ); -} - -// -// Toggle the control center window's visibility. -// -void foobar_application_toggle_control_center( FoobarApplication* self ) -{ - g_return_if_fail( FOOBAR_IS_APPLICATION( self ) ); - - gboolean previously_visible = gtk_widget_is_visible( GTK_WIDGET( self->control_center ) ); - gtk_widget_set_visible( GTK_WIDGET( self->control_center ), !previously_visible ); -} - -// -// Apply the general application configuration provided by the configuration service. -// -void foobar_application_apply_configuration( - FoobarApplication* self, - FoobarGeneralConfiguration const* config ) -{ - g_return_if_fail( FOOBAR_IS_APPLICATION( self ) ); - g_return_if_fail( config != NULL ); - - GdkDisplay* display = gdk_display_get_default( ); - - // Remove the old stylesheet from the application. - - if ( self->style_provider ) - { - gtk_style_context_remove_provider_for_display( display, GTK_STYLE_PROVIDER( self->style_provider ) ); - g_clear_object( &self->style_provider ); - } - - // Add the new stylesheet. - - g_autoptr( GFile ) file = g_file_new_for_uri( foobar_general_configuration_get_stylesheet( config ) ); - self->style_provider = gtk_css_provider_new( ); - gtk_css_provider_load_from_file( self->style_provider, file ); - gtk_style_context_add_provider_for_display( - display, - GTK_STYLE_PROVIDER( self->style_provider ), - GTK_STYLE_PROVIDER_PRIORITY_APPLICATION ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called after the application server process has asynchronously acquired the bus. -// -// This is where we can export server functionality using the corresponding DBus skeleton object. -// -void foobar_application_handle_bus_acquired( - GDBusConnection* connection, - gchar const* name, - gpointer userdata ) -{ - (void)name; - FoobarApplication* self = (FoobarApplication*)userdata; - - self->server_skeleton = foobar_server_skeleton_new( ); - g_signal_connect( - self->server_skeleton, - "handle-inspector", - G_CALLBACK( foobar_application_handle_inspector ), - self ); - g_signal_connect( - self->server_skeleton, - "handle-quit", - G_CALLBACK( foobar_application_handle_quit ), - self ); - g_signal_connect( - self->server_skeleton, - "handle-toggle-launcher", - G_CALLBACK( foobar_application_handle_toggle_launcher ), - self ); - g_signal_connect( - self->server_skeleton, - "handle-toggle-control-center", - G_CALLBACK( foobar_application_handle_toggle_control_center ), - self ); - - g_autoptr( GError ) error = NULL; - if ( !g_dbus_interface_skeleton_export( - G_DBUS_INTERFACE_SKELETON( self->server_skeleton ), - connection, - "/com/github/hannesschulze/foobar/server", - &error ) ) - { - g_warning( "Unable to export server interface: %s", error->message ); - } -} - -// -// DBus skeleton callback for the "Inspector" method. -// -gboolean foobar_application_handle_inspector( - FoobarServer* server, - GDBusMethodInvocation* invocation, - gpointer userdata ) -{ - (void)userdata; - - gtk_window_set_interactive_debugging( TRUE ); - - foobar_server_complete_inspector( server, invocation ); - return G_DBUS_METHOD_INVOCATION_HANDLED; -} - -// -// DBus skeleton callback for the "Quit" method. -// -gboolean foobar_application_handle_quit( - FoobarServer* server, - GDBusMethodInvocation* invocation, - gpointer userdata ) -{ - FoobarApplication* self = (FoobarApplication*)userdata; - - foobar_application_destroy_panels( self ); - - foobar_server_complete_quit( server, invocation ); - return G_DBUS_METHOD_INVOCATION_HANDLED; -} - -// -// DBus skeleton callback for the "ToggleLauncher" method. -// -gboolean foobar_application_handle_toggle_launcher( - FoobarServer* server, - GDBusMethodInvocation* invocation, - gpointer userdata ) -{ - FoobarApplication* self = (FoobarApplication*)userdata; - - foobar_application_toggle_launcher( self ); - - foobar_server_complete_toggle_launcher( server, invocation ); - return G_DBUS_METHOD_INVOCATION_HANDLED; -} - -// -// DBus skeleton callback for the "ToggleControlCenter" method. -// -gboolean foobar_application_handle_toggle_control_center( - FoobarServer* server, - GDBusMethodInvocation* invocation, - gpointer userdata ) -{ - FoobarApplication* self = (FoobarApplication*)userdata; - - foobar_application_toggle_control_center( self ); - - foobar_server_complete_toggle_control_center( server, invocation ); - return G_DBUS_METHOD_INVOCATION_HANDLED; -} - -// -// Signal handler called when the global configuration file has changed. -// -void foobar_application_handle_config_changed( - GObject* object, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)object; - (void)pspec; - FoobarApplication* self = (FoobarApplication*)userdata; - - // Apply general settings. - - FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); - foobar_application_apply_configuration( self, foobar_configuration_get_general( config ) ); - - // Update panel instances depending on whether multi-monitor mode is enabled. - - FoobarPanelConfiguration const* panel_config = foobar_configuration_get_panel( config ); - gboolean panel_is_multi_monitor = foobar_panel_configuration_get_multi_monitor( panel_config ); - if ( self->panel_is_multi_monitor != panel_is_multi_monitor ) - { - self->panel_is_multi_monitor = panel_is_multi_monitor; - - // Panel windows keep the application alive, so we need to "hold" it while we re-create the panel windows. - - g_application_hold( G_APPLICATION( self ) ); - foobar_application_destroy_panels( self ); - foobar_application_create_panels( self ); - g_application_release( G_APPLICATION( self ) ); - } -} - -// -// Signal handler called when the list of GdkMonitor objects has changed, possibly requiring us to re-create the panel -// windows. -// -void foobar_application_handle_monitors_changed( - GListModel* list, - guint position, - guint removed, - guint added, - gpointer userdata ) -{ - (void)list; - (void)position; - (void)removed; - (void)added; - FoobarApplication* self = (FoobarApplication*)userdata; - - if ( self->panel_is_multi_monitor ) - { - // Panel windows keep the application alive, so we need to "hold" it while we re-create the panel windows. - - g_application_hold( G_APPLICATION( self ) ); - foobar_application_destroy_panels( self ); - foobar_application_create_panels( self ); - g_application_release( G_APPLICATION( self ) ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Methods -// --------------------------------------------------------------------------------------------------------------------- - -// -// Destroy all currently active panel windows. -// -void foobar_application_destroy_panels( FoobarApplication* self ) -{ - for ( guint i = 0; i < self->panel_window_ids->len; ++i ) - { - guint window_id = g_array_index( self->panel_window_ids, guint, i ); - GtkWindow* window = gtk_application_get_window_by_id( GTK_APPLICATION( self ), window_id ); - if ( window ) { gtk_window_destroy( window ); } - } - - g_clear_pointer( &self->panel_window_ids, g_array_unref ); -} - -// -// Create and present panel windows for -// - each monitor, if multi-monitor mode is enabled -// - the current monitor, if multi-monitor mode is disabled. -// -void foobar_application_create_panels( FoobarApplication* self ) -{ - self->panel_window_ids = g_array_new( FALSE, FALSE, sizeof( guint ) ); - - if ( self->panel_is_multi_monitor ) - { - for ( guint i = 0; i < g_list_model_get_n_items( self->monitors ); ++i ) - { - GdkMonitor* monitor = g_list_model_get_item( self->monitors, i ); - guint id = foobar_application_create_panel( self, monitor ); - g_array_append_val( self->panel_window_ids, id ); - } - } - else - { - guint id = foobar_application_create_panel( self, NULL ); - g_array_append_val( self->panel_window_ids, id ); - } -} - -// -// Create and present a panel for a single monitor (or NULL to use the current monitor). -// -guint foobar_application_create_panel( - FoobarApplication* self, - GdkMonitor* monitor ) -{ - FoobarPanel* panel = foobar_panel_new( - GTK_APPLICATION( self ), - monitor, - self->battery_service, - self->clock_service, - self->brightness_service, - self->workspace_service, - self->audio_service, - self->network_service, - self->bluetooth_service, - self->notification_service, - self->configuration_service ); - gtk_window_present( GTK_WINDOW( panel ) ); - - return gtk_application_window_get_id( GTK_APPLICATION_WINDOW( panel ) ); -} diff --git a/src/application.h b/src/application.h deleted file mode 100644 index c5353c3..0000000 --- a/src/application.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include "services/configuration-service.h" - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_APPLICATION foobar_application_get_type( ) -#define FOOBAR_APPLICATION_DEFAULT FOOBAR_APPLICATION( g_application_get_default( ) ) - -G_DECLARE_FINAL_TYPE( FoobarApplication, foobar_application, FOOBAR, APPLICATION, GtkApplication ) - -FoobarApplication* foobar_application_new ( void ); -void foobar_application_toggle_launcher ( FoobarApplication* self ); -void foobar_application_toggle_control_center( FoobarApplication* self ); -void foobar_application_apply_configuration ( FoobarApplication* self, - FoobarGeneralConfiguration const* config ); - -G_END_DECLS diff --git a/src/control-center.c b/src/control-center.c deleted file mode 100644 index bb22830..0000000 --- a/src/control-center.c +++ /dev/null @@ -1,821 +0,0 @@ -#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 - -#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 ); -} diff --git a/src/control-center.h b/src/control-center.h deleted file mode 100644 index c320017..0000000 --- a/src/control-center.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include -#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" -#include "services/configuration-service.h" - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_CONTROL_CENTER foobar_control_center_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarControlCenter, foobar_control_center, FOOBAR, CONTROL_CENTER, GtkWindow ) - -FoobarControlCenter* foobar_control_center_new ( FoobarBrightnessService* brightness_service, - FoobarAudioService* audio_service, - FoobarNetworkService* network_service, - FoobarBluetoothService* bluetooth_service, - FoobarNotificationService* notification_service, - FoobarConfigurationService* configuration_service ); -void foobar_control_center_apply_configuration( FoobarControlCenter* self, - FoobarControlCenterConfiguration const* config, - FoobarNotificationConfiguration const* notification_config ); - -G_END_DECLS diff --git a/src/dbus/com.github.hannesschulze.foobar.xml b/src/dbus/com.github.hannesschulze.foobar.xml deleted file mode 100644 index e8373a6..0000000 --- a/src/dbus/com.github.hannesschulze.foobar.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/dbus/meson.build b/src/dbus/meson.build deleted file mode 100644 index 80062bd..0000000 --- a/src/dbus/meson.build +++ /dev/null @@ -1,37 +0,0 @@ -foobar_sources += gnome.gdbus_codegen( - 'server', - 'com.github.hannesschulze.foobar.xml', - namespace: 'Foobar', - annotations: [ - ['com.github.hannesschulze.foobar.Server', 'org.gtk.GDBus.C.Name', 'Server'], - ], -) - -foobar_sources += gnome.gdbus_codegen( - 'notifications', - 'org.freedesktop.Notifications.xml', - namespace: 'Foobar', - annotations: [ - ['org.freedesktop.Notifications', 'org.gtk.GDBus.C.Name', 'Notifications'], - ], -) - -foobar_sources += gnome.gdbus_codegen( - 'upower', - 'org.freedesktop.UPower.xml', - namespace: 'Foobar', - annotations: [ - ['org.freedesktop.UPower.Device', 'org.gtk.GDBus.C.Name', 'UPowerDevice'], - ], -) - -foobar_sources += gnome.gdbus_codegen( - 'bluez', - 'org.bluez.xml', - namespace: 'Foobar', - annotations: [ - ['org.bluez.Adapter1', 'org.gtk.GDBus.C.Name', 'BluezAdapter'], - ['org.bluez.Device1', 'org.gtk.GDBus.C.Name', 'BluezDevice'], - ['org.bluez.AgentManager1', 'org.gtk.GDBus.C.Name', 'BluezAgentManager'], - ], -) diff --git a/src/dbus/org.bluez.xml b/src/dbus/org.bluez.xml deleted file mode 100644 index 669e325..0000000 --- a/src/dbus/org.bluez.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/dbus/org.freedesktop.Notifications.xml b/src/dbus/org.freedesktop.Notifications.xml deleted file mode 100644 index 6b1d2f2..0000000 --- a/src/dbus/org.freedesktop.Notifications.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/dbus/org.freedesktop.UPower.xml b/src/dbus/org.freedesktop.UPower.xml deleted file mode 100644 index 18fc41d..0000000 --- a/src/dbus/org.freedesktop.UPower.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/launcher-item.c b/src/launcher-item.c deleted file mode 100644 index c4de70b..0000000 --- a/src/launcher-item.c +++ /dev/null @@ -1,73 +0,0 @@ -#include "launcher-item.h" - -static void foobar_launcher_item_default_init( FoobarLauncherItemInterface* iface ); - -G_DEFINE_INTERFACE( FoobarLauncherItem, foobar_launcher_item, G_TYPE_OBJECT ) - -// -// Static initialization for the launcher item interface. -// -void foobar_launcher_item_default_init( FoobarLauncherItemInterface* iface ) -{ - g_object_interface_install_property( iface, g_param_spec_string( - "title", - "Title", - "The title text displayed in the launcher.", - NULL, - G_PARAM_READABLE ) ); - g_object_interface_install_property( iface, g_param_spec_string( - "description", - "Description", - "Text displayed below the title.", - NULL, - G_PARAM_READABLE ) ); - g_object_interface_install_property( iface, g_param_spec_object( - "icon", - "Icon", - "Icon to show for the item.", - G_TYPE_ICON, - G_PARAM_READABLE ) ); -} - -// -// Get the title text displayed in the launcher. -// -gchar const* foobar_launcher_item_get_title( FoobarLauncherItem* self ) -{ - g_return_val_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ), NULL ); - FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self ); - g_return_val_if_fail( iface->get_title != NULL, NULL ); - return iface->get_title( self ); -} - -// -// Get the text displayed below the title. -// -gchar const* foobar_launcher_item_get_description( FoobarLauncherItem* self ) -{ - g_return_val_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ), NULL ); - FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self ); - g_return_val_if_fail( iface->get_description != NULL, NULL ); - return iface->get_description( self ); -} - -// -// Get the icon to show for the item. -// -GIcon* foobar_launcher_item_get_icon( FoobarLauncherItem* self ) -{ - g_return_val_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ), NULL ); - FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self ); - g_return_val_if_fail( iface->get_icon != NULL, NULL ); - return iface->get_icon( self ); -} - -// -// Activate the item, e.g. by launching the application. -// -void foobar_launcher_item_activate( FoobarLauncherItem* self ) -{ - g_return_if_fail( FOOBAR_IS_LAUNCHER_ITEM( self ) ); - FoobarLauncherItemInterface* iface = FOOBAR_LAUNCHER_ITEM_GET_IFACE( self ); - if ( iface->activate ) { iface->activate( self ); } -} diff --git a/src/launcher-item.h b/src/launcher-item.h deleted file mode 100644 index b2d274a..0000000 --- a/src/launcher-item.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_LAUNCHER_ITEM foobar_launcher_item_get_type( ) - -G_DECLARE_INTERFACE( FoobarLauncherItem, foobar_launcher_item, FOOBAR, LAUNCHER_ITEM, GObject ) - -struct _FoobarLauncherItemInterface -{ - GTypeInterface parent_interface; - - gchar const* ( *get_title ) ( FoobarLauncherItem* self ); - gchar const* ( *get_description )( FoobarLauncherItem* self ); - GIcon* ( *get_icon ) ( FoobarLauncherItem* self ); - void ( *activate ) ( FoobarLauncherItem* self ); -}; - -gchar const* foobar_launcher_item_get_title ( FoobarLauncherItem* self ); -gchar const* foobar_launcher_item_get_description( FoobarLauncherItem* self ); -GIcon* foobar_launcher_item_get_icon ( FoobarLauncherItem* self ); -void foobar_launcher_item_activate ( FoobarLauncherItem* self ); - -G_END_DECLS - diff --git a/src/launcher.c b/src/launcher.c deleted file mode 100644 index f4b34d8..0000000 --- a/src/launcher.c +++ /dev/null @@ -1,648 +0,0 @@ -#include "launcher.h" -#include "launcher-item.h" -#include "widgets/limit-container.h" -#include -#include -#include - -// -// FoobarLauncher: -// -// A window allowing the user to quickly search for applications and launch them. -// -// Note that it should be possible to continue typing, even when the results list view is focused. Conversely, the arrow -// keys can be used to select an item, even when the search input is focused. -// - -struct _FoobarLauncher -{ - GtkWindow parent_instance; - gchar** search_terms; - GtkWidget* search_text; - GtkWidget* list_view; - GtkWidget* limit_container; - GtkFilterListModel* filter_model; - GListStore* quick_answer_model; - GtkFlattenListModel* flatten_model; - GtkSingleSelection* selection_model; - FoobarApplicationService* application_service; - FoobarQuickAnswerService* quick_answer_service; - FoobarConfigurationService* configuration_service; - gulong config_handler_id; -}; - -static void foobar_launcher_class_init ( FoobarLauncherClass* klass ); -static void foobar_launcher_init ( FoobarLauncher* self ); -static void foobar_launcher_finalize ( GObject* object ); -static void foobar_launcher_handle_search_changed ( GtkEditable* editable, - gpointer userdata ); -static void foobar_launcher_handle_search_activate ( GtkText* text, - gpointer userdata ); -static gboolean foobar_launcher_handle_search_key ( GtkEventControllerKey* controller, - guint keyval, - guint keycode, - GdkModifierType state, - gpointer userdata ); -static gboolean foobar_launcher_handle_list_key ( GtkEventControllerKey* controller, - guint keyval, - guint keycode, - GdkModifierType state, - gpointer userdata ); -static gboolean foobar_launcher_handle_window_key ( GtkEventControllerKey* controller, - guint keyval, - guint keycode, - GdkModifierType state, - gpointer userdata ); -static void foobar_launcher_handle_item_setup ( GtkListItemFactory* factory, - GtkListItem* list_item, - gpointer userdata ); -static void foobar_launcher_handle_item_activate ( GtkListView* view, - guint position, - gpointer userdata ); -static void foobar_launcher_handle_config_change ( GObject* object, - GParamSpec* pspec, - gpointer userdata ); -static void foobar_launcher_handle_show ( GtkWidget* widget, - gpointer userdata ); -static gboolean foobar_launcher_compute_icon_visible ( GtkExpression* expression, - GIcon* icon, - gpointer userdata ); -static gboolean foobar_launcher_compute_label_visible ( GtkExpression* expression, - gchar const* label, - gpointer userdata ); -static gboolean foobar_launcher_compute_separator_visible( GtkExpression* expression, - guint item_count, - gpointer userdata ); -static gboolean foobar_launcher_filter_func ( gpointer item, - gpointer userdata ); -static gboolean foobar_launcher_is_navigation_key ( guint keyval ); - -G_DEFINE_FINAL_TYPE( FoobarLauncher, foobar_launcher, GTK_TYPE_WINDOW ) - -// --------------------------------------------------------------------------------------------------------------------- -// Window Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the launcher. -// -void foobar_launcher_class_init( FoobarLauncherClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->finalize = foobar_launcher_finalize; -} - -// -// Instance initialization for the launcher. -// -void foobar_launcher_init( FoobarLauncher* self ) -{ - self->search_terms = g_new0( gchar*, 1 ); - - // Set up the search input and an event controller for auto-switching focus to the result list. - - GtkEventController* search_controller = gtk_event_controller_key_new( ); - g_signal_connect( search_controller, "key-pressed", G_CALLBACK( foobar_launcher_handle_search_key ), self ); - g_signal_connect( search_controller, "key-released", G_CALLBACK( foobar_launcher_handle_search_key ), self ); - - self->search_text = gtk_text_new( ); - gtk_text_set_placeholder_text( GTK_TEXT( self->search_text ), "Search…" ); - gtk_widget_add_controller( self->search_text, search_controller ); - gtk_widget_set_hexpand( self->search_text, TRUE ); - g_signal_connect( self->search_text, "changed", G_CALLBACK( foobar_launcher_handle_search_changed ), self ); - g_signal_connect( self->search_text, "activate", G_CALLBACK( foobar_launcher_handle_search_activate ), self ); - - GtkWidget* search_icon = gtk_image_new_from_icon_name( "fluent-search-symbolic" ); - - GtkWidget* search = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); - gtk_box_append( GTK_BOX( search ), search_icon ); - gtk_box_append( GTK_BOX( search ), self->search_text ); - gtk_widget_add_css_class( search, "search" ); - - // Set up the separator (only shown if the results are not empty). - - GtkWidget *separator = gtk_separator_new( GTK_ORIENTATION_HORIZONTAL ); - - // Set up the results list view and an event controller for auto-switching focus to the input. - - GtkListItemFactory* item_factory = gtk_signal_list_item_factory_new( ); - g_signal_connect( item_factory, "setup", G_CALLBACK( foobar_launcher_handle_item_setup ), NULL ); - - GtkCustomFilter* filter = gtk_custom_filter_new( foobar_launcher_filter_func, self, NULL ); - - self->filter_model = gtk_filter_list_model_new( NULL, GTK_FILTER( filter ) ); - - self->quick_answer_model = g_list_store_new( FOOBAR_TYPE_LAUNCHER_ITEM ); - - GListStore* flattened_models = g_list_store_new( G_TYPE_LIST_MODEL ); - g_list_store_append( flattened_models, self->quick_answer_model ); - g_list_store_append( flattened_models, self->filter_model ); - - self->flatten_model = gtk_flatten_list_model_new( G_LIST_MODEL( flattened_models ) ); - - self->selection_model = gtk_single_selection_new( G_LIST_MODEL( g_object_ref( self->flatten_model ) ) ); - - { - GtkExpression* list_expr = gtk_constant_expression_new( GTK_TYPE_FLATTEN_LIST_MODEL, self->flatten_model ); - GtkExpression* count_expr = gtk_property_expression_new( GTK_TYPE_FLATTEN_LIST_MODEL, list_expr, "n-items" ); - GtkExpression* visible_params[] = { count_expr }; - GtkExpression* visible_expr = gtk_cclosure_expression_new( - G_TYPE_BOOLEAN, - NULL, - G_N_ELEMENTS( visible_params ), - visible_params, - G_CALLBACK( foobar_launcher_compute_separator_visible ), - NULL, - NULL ); - gtk_expression_bind( visible_expr, separator, "visible", NULL ); - } - - GtkEventController* list_controller = gtk_event_controller_key_new( ); - g_signal_connect( list_controller, "key-pressed", G_CALLBACK( foobar_launcher_handle_list_key ), self ); - g_signal_connect( list_controller, "key-released", G_CALLBACK( foobar_launcher_handle_list_key ), self ); - - self->list_view = gtk_list_view_new( GTK_SELECTION_MODEL( g_object_ref( self->selection_model ) ), item_factory ); - gtk_list_view_set_single_click_activate( GTK_LIST_VIEW( self->list_view ), TRUE ); - gtk_scrollable_set_vscroll_policy( GTK_SCROLLABLE( self->list_view ), GTK_SCROLL_MINIMUM ); - gtk_widget_add_controller( self->list_view, list_controller ); - g_signal_connect( self->list_view, "activate", G_CALLBACK( foobar_launcher_handle_item_activate ), self ); - - GtkWidget* scrolled_window = gtk_scrolled_window_new( ); - // XXX: Use GTK_POLICY_AUTOMATIC instead of GTK_POLICY_EXTERNAL once I figured out how to remove the minimum - // height in this case. For now, no scrollbar is shown. - gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scrolled_window ), GTK_POLICY_NEVER, GTK_POLICY_EXTERNAL ); - gtk_scrolled_window_set_child( GTK_SCROLLED_WINDOW( scrolled_window ), self->list_view ); - gtk_scrolled_window_set_propagate_natural_height( GTK_SCROLLED_WINDOW( scrolled_window ), TRUE ); - gtk_widget_set_vexpand( scrolled_window, TRUE ); - - // Set up the layout. - - GtkWidget* layout = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); - gtk_box_append( GTK_BOX( layout ), search ); - gtk_box_append( GTK_BOX( layout ), separator ); - gtk_box_append( GTK_BOX( layout ), scrolled_window ); - - // Let the launcher take up space to accommodate the result list's natural height until the configured maximum - // height. - - self->limit_container = foobar_limit_container_new( ); - foobar_limit_container_set_child( FOOBAR_LIMIT_CONTAINER( self->limit_container ), layout ); - - // Set up the window, listening for the "Escape" key. - - GtkEventController* window_controller = gtk_event_controller_key_new( ); - g_signal_connect( window_controller, "key-released", G_CALLBACK( foobar_launcher_handle_window_key ), self ); - - gtk_window_set_child( GTK_WINDOW( self ), self->limit_container ); - gtk_widget_add_controller( GTK_WIDGET( self ), window_controller ); - gtk_window_set_title( GTK_WINDOW( self ), "Foobar Launcher" ); - gtk_widget_add_css_class( GTK_WIDGET( self ), "launcher" ); - gtk_layer_init_for_window( GTK_WINDOW( self ) ); - gtk_layer_set_keyboard_mode( GTK_WINDOW( self ), GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE ); - gtk_layer_set_namespace( GTK_WINDOW( self ), "foobar-launcher" ); - gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, TRUE ); - g_signal_connect( self, "show", G_CALLBACK( foobar_launcher_handle_show ), self ); -} - -// -// Instance cleanup for the launcher. -// -void foobar_launcher_finalize( GObject* object ) -{ - FoobarLauncher* self = (FoobarLauncher*)object; - - g_clear_signal_handler( &self->config_handler_id, self->configuration_service ); - g_clear_object( &self->filter_model ); - g_clear_object( &self->quick_answer_model ); - g_clear_object( &self->flatten_model ); - g_clear_object( &self->selection_model ); - g_clear_object( &self->application_service ); - g_clear_object( &self->quick_answer_service ); - g_clear_object( &self->configuration_service ); - g_clear_pointer( &self->search_terms, g_strfreev ); - - G_OBJECT_CLASS( foobar_launcher_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new launcher instance. -// -FoobarLauncher* foobar_launcher_new( - FoobarApplicationService* application_service, - FoobarQuickAnswerService* quick_answer_service, - FoobarConfigurationService* configuration_service ) -{ - g_return_val_if_fail( FOOBAR_IS_APPLICATION_SERVICE( application_service ), NULL ); - g_return_val_if_fail( FOOBAR_IS_QUICK_ANSWER_SERVICE( quick_answer_service ), NULL ); - g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( configuration_service ), NULL ); - - FoobarLauncher* self = g_object_new( FOOBAR_TYPE_LAUNCHER, NULL ); - self->application_service = g_object_ref( application_service ); - self->quick_answer_service = g_object_ref( quick_answer_service ); - self->configuration_service = g_object_ref( configuration_service ); - - // Set up the result list view's source model. - - GListModel* source_model = foobar_application_service_get_items( self->application_service ); - gtk_filter_list_model_set_model( self->filter_model, g_object_ref( source_model ) ); - - // Apply the configuration and subscribe to changes. - - FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); - foobar_launcher_apply_configuration( self, foobar_configuration_get_launcher( config ) ); - self->config_handler_id = g_signal_connect( - self->configuration_service, - "notify::current", - G_CALLBACK( foobar_launcher_handle_config_change ), - self ); - - return self; -} - -// -// Apply the launcher configuration provided by the configuration service. -// -void foobar_launcher_apply_configuration( - FoobarLauncher* self, - FoobarLauncherConfiguration const* config ) -{ - g_return_if_fail( FOOBAR_IS_LAUNCHER( self ) ); - g_return_if_fail( config != NULL ); - - gtk_window_set_default_size( GTK_WINDOW( self ), foobar_launcher_configuration_get_width( config ), 2 ); - gtk_layer_set_margin( - GTK_WINDOW( self ), - GTK_LAYER_SHELL_EDGE_TOP, - foobar_launcher_configuration_get_position( config ) ); - foobar_limit_container_set_max_height( - FOOBAR_LIMIT_CONTAINER( self->limit_container ), - foobar_launcher_configuration_get_max_height( config ) ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called when the user confirms the selection while the search input is focused. -// -// We forward the activation to the results list view. -// -void foobar_launcher_handle_search_activate( - GtkText* text, - gpointer userdata ) -{ - (void)text; - FoobarLauncher* self = (FoobarLauncher*)userdata; - - guint selection = gtk_single_selection_get_selected( self->selection_model ); - if ( selection != GTK_INVALID_LIST_POSITION ) - { - gtk_widget_activate_action( self->list_view, "list.activate-item", "u", selection ); - } -} - -// -// Called when the search query has changed. -// -// We update the tokenized search terms and then the result filter. -// -void foobar_launcher_handle_search_changed( - GtkEditable* editable, - gpointer userdata ) -{ - FoobarLauncher* self = (FoobarLauncher*)userdata; - - // Update quick answer. - - g_autoptr(FoobarQuickAnswer) answer = foobar_quick_answer_service_query( self->quick_answer_service, gtk_editable_get_text( editable ) ); - g_list_store_remove_all( self->quick_answer_model ); - if (answer) - { - g_list_store_append(self->quick_answer_model, answer); - } - - // Update filter. - - GStrvBuilder* terms_builder = g_strv_builder_new( ); - g_autofree gchar* query = g_strdup( gtk_editable_get_text( editable ) ); - gchar* query_start = query; - gchar* save; - gchar* token; - while ( ( token = strtok_r( query_start, " \t", &save ) ) ) - { - if ( *token ) { g_strv_builder_add( terms_builder, token ); } - query_start = NULL; - } - - g_clear_pointer( &self->search_terms, g_strfreev ); - self->search_terms = g_strv_builder_end( terms_builder ); - - GtkFilter* filter = gtk_filter_list_model_get_filter( self->filter_model ); - gtk_filter_changed( filter, GTK_FILTER_CHANGE_DIFFERENT ); -} - -// -// Handle keyboard events while the search input is focused. -// -// If this is a navigational key, the event is forwarded to the results list view. -// -gboolean foobar_launcher_handle_search_key( - GtkEventControllerKey* controller, - guint keyval, - guint keycode, - GdkModifierType state, - gpointer userdata ) -{ - (void)keycode; - (void)state; - FoobarLauncher* self = (FoobarLauncher*)userdata; - - if ( foobar_launcher_is_navigation_key( keyval ) ) - { - gtk_widget_grab_focus( self->list_view ); - return gtk_event_controller_key_forward( controller, self->list_view ); - } - else - { - return FALSE; - } -} - -// -// Handle keyboard events while the list view is focused. -// -// If this is not a navigational key, the event is forwarded to the search input, which is also focused. -// -gboolean foobar_launcher_handle_list_key( - GtkEventControllerKey* controller, - guint keyval, - guint keycode, - GdkModifierType state, - gpointer userdata ) -{ - (void)keycode; - (void)state; - FoobarLauncher* self = (FoobarLauncher*)userdata; - - if ( foobar_launcher_is_navigation_key( keyval ) ) - { - return FALSE; - } - else - { - gtk_text_grab_focus_without_selecting( GTK_TEXT( self->search_text ) ); - return gtk_event_controller_key_forward( controller, self->search_text ); - } -} - -// -// Handle general keyboard events for the window. -// -// This is used to allow the user to close the launcher by pressing the "Escape" key. -// -gboolean foobar_launcher_handle_window_key( - GtkEventControllerKey* controller, - guint keyval, - guint keycode, - GdkModifierType state, - gpointer userdata ) -{ - (void)controller; - (void)keycode; - (void)state; - FoobarLauncher* self = (FoobarLauncher*)userdata; - - if ( keyval == GDK_KEY_Escape ) - { - gtk_widget_set_visible( GTK_WIDGET( self ), FALSE ); - return TRUE; - } - else - { - return FALSE; - } -} - -// -// Called by the result list view to create a widget for displaying an application item. -// -void foobar_launcher_handle_item_setup( - GtkListItemFactory* factory, - GtkListItem* list_item, - gpointer userdata ) -{ - (void)factory; - (void)userdata; - - // Set up layout. - - GtkWidget* icon = gtk_image_new( ); - gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); - gtk_widget_add_css_class( icon, "icon" ); - - GtkWidget* title = gtk_label_new( NULL ); - gtk_label_set_justify( GTK_LABEL( title ), GTK_JUSTIFY_LEFT ); - gtk_label_set_xalign( GTK_LABEL( title ), 0 ); - gtk_label_set_wrap( GTK_LABEL( title ), FALSE ); - gtk_label_set_ellipsize( GTK_LABEL( title ), PANGO_ELLIPSIZE_END ); - gtk_widget_add_css_class( title, "name" ); - - GtkWidget* description = gtk_label_new( NULL ); - gtk_label_set_justify( GTK_LABEL( description ), GTK_JUSTIFY_LEFT ); - gtk_label_set_xalign( GTK_LABEL( description ), 0 ); - gtk_label_set_wrap( GTK_LABEL( description ), FALSE ); - gtk_label_set_ellipsize( GTK_LABEL( description ), PANGO_ELLIPSIZE_END ); - gtk_widget_add_css_class( description, "description" ); - - GtkWidget* column = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); - gtk_widget_set_hexpand( column, TRUE ); - gtk_widget_set_valign( column, GTK_ALIGN_CENTER ); - gtk_box_append( GTK_BOX( column ), title ); - gtk_box_append( GTK_BOX( column ), description ); - - GtkWidget* row = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); - gtk_widget_add_css_class( row, "item" ); - gtk_box_append( GTK_BOX( row ), icon ); - gtk_box_append( GTK_BOX( row ), column ); - - gtk_list_item_set_child( list_item, row ); - - // Set up bindings. - - { - GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" ); - GtkExpression* icon_expr = gtk_property_expression_new( FOOBAR_TYPE_LAUNCHER_ITEM, item_expr, "icon" ); - gtk_expression_bind( gtk_expression_ref( icon_expr ), icon, "gicon", list_item ); - GtkExpression* visible_params[] = { icon_expr }; - GtkExpression* visible_expr = gtk_cclosure_expression_new( - G_TYPE_BOOLEAN, - NULL, - G_N_ELEMENTS( visible_params ), - visible_params, - G_CALLBACK( foobar_launcher_compute_icon_visible ), - NULL, - NULL ); - gtk_expression_bind( visible_expr, icon, "visible", list_item ); - } - - { - GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" ); - GtkExpression* title_expr = gtk_property_expression_new( FOOBAR_TYPE_LAUNCHER_ITEM, item_expr, "title" ); - gtk_expression_bind( title_expr, title, "label", list_item ); - } - - { - GtkExpression* item_expr = gtk_property_expression_new( GTK_TYPE_LIST_ITEM, NULL, "item" ); - GtkExpression* description_expr = gtk_property_expression_new( FOOBAR_TYPE_LAUNCHER_ITEM, item_expr, "description" ); - gtk_expression_bind( gtk_expression_ref( description_expr ), description, "label", list_item ); - GtkExpression* visible_params[] = { description_expr }; - GtkExpression* visible_expr = gtk_cclosure_expression_new( - G_TYPE_BOOLEAN, - NULL, - G_N_ELEMENTS( visible_params ), - visible_params, - G_CALLBACK( foobar_launcher_compute_label_visible ), - NULL, - NULL ); - gtk_expression_bind( visible_expr, description, "visible", list_item ); - } -} - -// -// Called by the result list view when an application was selected. -// -void foobar_launcher_handle_item_activate( - GtkListView* view, - guint position, - gpointer userdata ) -{ - (void)view; - FoobarLauncher* self = (FoobarLauncher*)userdata; - - GListModel* list = G_LIST_MODEL( gtk_list_view_get_model( view ) ); - FoobarLauncherItem* item = g_list_model_get_item( list, position ); - foobar_launcher_item_activate( item ); - gtk_widget_set_visible( GTK_WIDGET( self ), FALSE ); -} - -// -// Signal handler called when the global configuration file has changed. -// -void foobar_launcher_handle_config_change( - GObject* object, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)object; - (void)pspec; - FoobarLauncher* self = (FoobarLauncher*)userdata; - - FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); - foobar_launcher_apply_configuration( self, foobar_configuration_get_launcher( config ) ); -} - -// -// Called before the launcher is presented. -// -// This is used to reset the UI state. -// -void foobar_launcher_handle_show( - GtkWidget* widget, - gpointer userdata ) -{ - (void)widget; - FoobarLauncher* self = (FoobarLauncher*)userdata; - - gtk_editable_set_text( GTK_EDITABLE( self->search_text ), "" ); - if ( g_list_model_get_n_items( G_LIST_MODEL( self->selection_model ) ) > 0 ) - { - gtk_list_view_scroll_to( - GTK_LIST_VIEW( self->list_view ), - 0, - GTK_LIST_SCROLL_FOCUS | GTK_LIST_SCROLL_SELECT, - NULL ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Value Converters -// --------------------------------------------------------------------------------------------------------------------- - -// -// Derive the visibility of an application icon from its value. -// -gboolean foobar_launcher_compute_icon_visible( - GtkExpression* expression, - GIcon* icon, - gpointer userdata ) -{ - (void)expression; - (void)userdata; - - return icon != NULL; -} - -// -// Derive the visibility of an application label from its value. -// -gboolean foobar_launcher_compute_label_visible( - GtkExpression* expression, - gchar const* label, - gpointer userdata ) -{ - (void)expression; - (void)userdata; - - return label != NULL; -} - -// -// Derive the visibility of the separator between search input and result list view from the number of results. -// -gboolean foobar_launcher_compute_separator_visible( - GtkExpression* expression, - guint item_count, - gpointer userdata ) -{ - (void)expression; - (void)userdata; - - return item_count > 0; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Methods -// --------------------------------------------------------------------------------------------------------------------- - -// -// Match a result item against the current search query. -// -gboolean foobar_launcher_filter_func( gpointer item, gpointer userdata ) -{ - FoobarLauncher* self = (FoobarLauncher*)userdata; - FoobarApplicationItem* app = (FoobarApplicationItem*)item; - - return foobar_application_item_match( app, (gchar const* const*)self->search_terms ); -} - -// -// Check if a key value is that of a navigation key (i.e., an arrow key). -// -gboolean foobar_launcher_is_navigation_key( guint keyval ) -{ - switch ( keyval ) - { - case GDK_KEY_Up: - case GDK_KEY_Down: - case GDK_KEY_Left: - case GDK_KEY_Right: - return TRUE; - default: - return FALSE; - } -} diff --git a/src/launcher.h b/src/launcher.h deleted file mode 100644 index 5d92607..0000000 --- a/src/launcher.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include -#include "services/application-service.h" -#include "services/configuration-service.h" -#include "services/quick-answer-service.h" - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_LAUNCHER foobar_launcher_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarLauncher, foobar_launcher, FOOBAR, LAUNCHER, GtkWindow ) - -FoobarLauncher* foobar_launcher_new ( FoobarApplicationService* application_service, - FoobarQuickAnswerService* quick_answer_service, - FoobarConfigurationService* configuration_service ); -void foobar_launcher_apply_configuration( FoobarLauncher* self, - FoobarLauncherConfiguration const* config ); - -G_END_DECLS diff --git a/src/main.c b/src/main.c deleted file mode 100644 index acabbdc..0000000 --- a/src/main.c +++ /dev/null @@ -1,12 +0,0 @@ -#include "application.h" - -// -// Entry point of the application, runs the GTK app. -// -int main( - int argc, - char** argv ) -{ - g_autoptr( FoobarApplication ) app = foobar_application_new( ); - return g_application_run( G_APPLICATION( app ), argc, argv ); -} diff --git a/src/meson.build b/src/meson.build deleted file mode 100644 index b9383fb..0000000 --- a/src/meson.build +++ /dev/null @@ -1,74 +0,0 @@ -foobar_sources = files( - 'application.c', - 'utils.c', - 'panel.c', - 'launcher.c', - 'launcher-item.c', - 'control-center.c', - 'notification-area.c', -) -foobar_tests = {} - -subdir('dbus') -subdir('services') -subdir('widgets') - -c_args = [ - '-Wall', - '-Werror', - '-Wswitch-enum', - '-Wswitch-default', - '-Wunused', - '-Wuninitialized', -] - -deps = [ - glib_dep, - gio_dep, - gio_unix_dep, - gtk_dep, - json_dep, - gmp_dep, - nm_dep, - m_dep, - layershell_dep, - gvc_dep, -] - -libfoobar = static_library( - meson.project_name(), - foobar_sources, - include_directories: include_directories('.'), - dependencies: deps, - install: false, - c_args: c_args -) - -libfoobar_dep = declare_dependency( - link_with: libfoobar, - include_directories: include_directories('.'), - dependencies: deps, -) - -executable( - meson.project_name(), - 'main.c', - styles_resource, - icons_resource, - dependencies: [ libfoobar_dep ], - install: true, - c_args: c_args -) - -if mutest_dep.found() - foreach name, sources : foobar_tests - test( - name, - executable( - 'foobar-test-' + name, - sources, - dependencies: [ libfoobar_dep, mutest_dep ], - ), - ) - endforeach -endif diff --git a/src/notification-area.c b/src/notification-area.c deleted file mode 100644 index 2db5397..0000000 --- a/src/notification-area.c +++ /dev/null @@ -1,196 +0,0 @@ -#include "notification-area.h" -#include "widgets/notification-widget.h" -#include - -// -// FoobarNotificationArea: -// -// A (usually invisible) window in the corner of the screen, displaying incoming notifications before they are either -// dismissed or a timeout has passed. -// - -struct _FoobarNotificationArea -{ - GtkWindow parent_instance; - GtkWidget* notification_list; - FoobarNotificationService* notification_service; - FoobarConfigurationService* configuration_service; - gint min_height; - gint spacing; - gint close_button_inset; - gchar* time_format; - gulong config_handler_id; -}; - -static void foobar_notification_area_class_init ( FoobarNotificationAreaClass* klass ); -static void foobar_notification_area_init ( FoobarNotificationArea* self ); -static void foobar_notification_area_finalize ( GObject* object ); -static void foobar_notification_area_handle_config_change( GObject* object, - GParamSpec* pspec, - gpointer userdata ); -static void foobar_notification_area_handle_item_setup ( GtkListItemFactory* factory, - GtkListItem* list_item, - gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarNotificationArea, foobar_notification_area, GTK_TYPE_WINDOW ) - -// --------------------------------------------------------------------------------------------------------------------- -// Window Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the notification area. -// -void foobar_notification_area_class_init( FoobarNotificationAreaClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->finalize = foobar_notification_area_finalize; -} - -// -// Instance initialization for the notification area. -// -void foobar_notification_area_init( FoobarNotificationArea* self ) -{ - GtkListItemFactory* notification_factory = gtk_signal_list_item_factory_new( ); - g_signal_connect( notification_factory, "setup", G_CALLBACK( foobar_notification_area_handle_item_setup ), self ); - self->notification_list = gtk_list_view_new( NULL, notification_factory ); - - gtk_window_set_child( GTK_WINDOW( self ), self->notification_list ); - gtk_window_set_title( GTK_WINDOW( self ), "Foobar Notification Area" ); - gtk_widget_add_css_class( GTK_WIDGET( self ), "notification-area" ); - gtk_layer_init_for_window( GTK_WINDOW( self ) ); - gtk_layer_set_namespace( GTK_WINDOW( self ), "foobar-notification-area" ); - gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_RIGHT, TRUE ); - gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, TRUE ); -} - -// -// Instance cleanup for the notification area. -// -void foobar_notification_area_finalize( GObject* object ) -{ - FoobarNotificationArea* self = (FoobarNotificationArea*)object; - - g_clear_signal_handler( &self->config_handler_id, self->configuration_service ); - g_clear_object( &self->notification_service ); - g_clear_object( &self->configuration_service ); - g_clear_pointer( &self->time_format, g_free ); - - G_OBJECT_CLASS( foobar_notification_area_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new notification area instance. -// -FoobarNotificationArea* foobar_notification_area_new( - FoobarNotificationService* notification_service, - FoobarConfigurationService* configuration_service ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( notification_service ), NULL ); - g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( configuration_service ), NULL ); - - FoobarNotificationArea* self = g_object_new( FOOBAR_TYPE_NOTIFICATION_AREA, NULL ); - 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_notification_area_apply_configuration( self, foobar_configuration_get_notifications( config ) ); - self->config_handler_id = g_signal_connect( - self->configuration_service, - "notify::current", - G_CALLBACK( foobar_notification_area_handle_config_change ), - self ); - - // Set up the notifications list view. - - GListModel* source_model = foobar_notification_service_get_popup_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 ) ); - - return self; -} - -// -// Apply the notification configuration provided by the configuration service. -// -void foobar_notification_area_apply_configuration( - FoobarNotificationArea* self, - FoobarNotificationConfiguration const* config ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION_AREA( self ) ); - g_return_if_fail( config != NULL ); - - // Copy configuration into member variables. - - gint width = foobar_notification_configuration_get_width( config ); - self->min_height = foobar_notification_configuration_get_min_height( config ); - self->spacing = foobar_notification_configuration_get_spacing( config ); - self->close_button_inset = foobar_notification_configuration_get_close_button_inset( config ); - g_clear_pointer( &self->time_format, g_free ); - self->time_format = g_strdup( foobar_notification_configuration_get_time_format( config ) ); - - // Recreate 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 ); - - // We need to use height 2, otherwise it crashes for some reason. - - gtk_window_set_default_size( GTK_WINDOW( self ), width + self->spacing, 2 ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Signal handler called when the global configuration file has changed. -// -void foobar_notification_area_handle_config_change( - GObject* object, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)object; - (void)pspec; - FoobarNotificationArea* self = (FoobarNotificationArea*)userdata; - - FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); - foobar_notification_area_apply_configuration( self, foobar_configuration_get_notifications( config ) ); -} - -// -// Called by the notification list view to create a widget for displaying a notification. -// -void foobar_notification_area_handle_item_setup( - GtkListItemFactory* factory, - GtkListItem* list_item, - gpointer userdata ) -{ - (void)factory; - FoobarNotificationArea* self = (FoobarNotificationArea*)userdata; - - GtkWidget* widget = foobar_notification_widget_new( ); - foobar_notification_widget_set_close_action( FOOBAR_NOTIFICATION_WIDGET( widget ), FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS ); - foobar_notification_widget_set_time_format( FOOBAR_NOTIFICATION_WIDGET( widget ), self->time_format ); - foobar_notification_widget_set_min_height( FOOBAR_NOTIFICATION_WIDGET( widget ), self->min_height ); - foobar_notification_widget_set_close_button_inset( FOOBAR_NOTIFICATION_WIDGET( widget ), self->close_button_inset ); - foobar_notification_widget_set_inset_end( FOOBAR_NOTIFICATION_WIDGET( widget ), self->spacing ); - foobar_notification_widget_set_inset_top( FOOBAR_NOTIFICATION_WIDGET( widget ), self->spacing ); - 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 ); - } -} diff --git a/src/notification-area.h b/src/notification-area.h deleted file mode 100644 index e2e546f..0000000 --- a/src/notification-area.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include -#include "services/notification-service.h" -#include "services/configuration-service.h" - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_NOTIFICATION_AREA foobar_notification_area_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarNotificationArea, foobar_notification_area, FOOBAR, NOTIFICATION_AREA, GtkWindow ) - -FoobarNotificationArea* foobar_notification_area_new ( FoobarNotificationService* notification_service, - FoobarConfigurationService* configuration_service ); -void foobar_notification_area_apply_configuration( FoobarNotificationArea* self, - FoobarNotificationConfiguration const* config ); - -G_END_DECLS diff --git a/src/panel.c b/src/panel.c deleted file mode 100644 index 5bc7662..0000000 --- a/src/panel.c +++ /dev/null @@ -1,348 +0,0 @@ -#include "panel.h" -#include "widgets/panel/panel-item-icon.h" -#include "widgets/panel/panel-item-clock.h" -#include "widgets/panel/panel-item-workspaces.h" -#include "widgets/panel/panel-item-status.h" -#include - -// -// FoobarPanel: -// -// A panel that is divided into start, center and end sections. Each section can be configured to present a range of -// items. -// - -struct _FoobarPanel -{ - GtkWindow parent_instance; - GtkWidget* main_layout; - GtkWidget* start_items; - GtkWidget* center_items; - GtkWidget* end_items; - GPtrArray* item_widgets; - GdkMonitor* monitor; - FoobarBatteryService* battery_service; - FoobarClockService* clock_service; - FoobarBrightnessService* brightness_service; - FoobarWorkspaceService* workspace_service; - FoobarAudioService* audio_service; - FoobarNetworkService* network_service; - FoobarBluetoothService* bluetooth_service; - FoobarNotificationService* notification_service; - FoobarConfigurationService* configuration_service; - gulong config_handler_id; -}; - -static void foobar_panel_class_init ( FoobarPanelClass* klass ); -static void foobar_panel_init ( FoobarPanel* self ); -static void foobar_panel_finalize ( GObject* object ); -static void foobar_panel_handle_config_change( GObject* object, - GParamSpec* pspec, - gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarPanel, foobar_panel, GTK_TYPE_APPLICATION_WINDOW ) - -// --------------------------------------------------------------------------------------------------------------------- -// Window Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the panel. -// -void foobar_panel_class_init( FoobarPanelClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->finalize = foobar_panel_finalize; -} - -// -// Instance initialization for the panel. -// -void foobar_panel_init( FoobarPanel* self ) -{ - self->start_items = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); - self->center_items = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); - self->end_items = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); - - self->main_layout = gtk_center_box_new( ); - gtk_center_box_set_start_widget( GTK_CENTER_BOX( self->main_layout ), self->start_items ); - gtk_center_box_set_center_widget( GTK_CENTER_BOX( self->main_layout ), self->center_items ); - gtk_center_box_set_end_widget( GTK_CENTER_BOX( self->main_layout ), self->end_items ); - - gtk_window_set_child( GTK_WINDOW( self ), self->main_layout ); - gtk_window_set_title( GTK_WINDOW( self ), "Foobar Panel" ); - gtk_widget_add_css_class( GTK_WIDGET( self ), "panel" ); - gtk_layer_init_for_window( GTK_WINDOW( self ) ); - gtk_layer_set_namespace( GTK_WINDOW( self ), "foobar-panel" ); - gtk_layer_auto_exclusive_zone_enable( GTK_WINDOW( self ) ); -} - -// -// Instance cleanup for the panel. -// -void foobar_panel_finalize( GObject* object ) -{ - FoobarPanel* self = (FoobarPanel*)object; - - g_clear_signal_handler( &self->config_handler_id, self->configuration_service ); - g_clear_object( &self->monitor ); - g_clear_object( &self->battery_service ); - g_clear_object( &self->clock_service ); - g_clear_object( &self->brightness_service ); - g_clear_object( &self->workspace_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_object( &self->configuration_service ); - g_clear_pointer( &self->item_widgets, g_ptr_array_unref ); - - G_OBJECT_CLASS( foobar_panel_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new panel instance. -// -// An optional GdkMonitor instance can be provided for multi-monitor setups. Otherwise, the default monitor is used. -// -FoobarPanel* foobar_panel_new( - GtkApplication* app, - GdkMonitor* monitor, - FoobarBatteryService* battery_service, - FoobarClockService* clock_service, - FoobarBrightnessService* brightness_service, - FoobarWorkspaceService* workspace_service, - FoobarAudioService* audio_service, - FoobarNetworkService* network_service, - FoobarBluetoothService* bluetooth_service, - FoobarNotificationService* notification_service, - FoobarConfigurationService* configuration_service ) -{ - g_return_val_if_fail( GTK_IS_APPLICATION( app ), NULL ); - g_return_val_if_fail( monitor == NULL || GDK_IS_MONITOR( monitor ), NULL ); - g_return_val_if_fail( FOOBAR_IS_BATTERY_SERVICE( battery_service ), NULL ); - g_return_val_if_fail( FOOBAR_IS_CLOCK_SERVICE( clock_service ), NULL ); - g_return_val_if_fail( FOOBAR_IS_BRIGHTNESS_SERVICE( brightness_service ), NULL ); - g_return_val_if_fail( FOOBAR_IS_WORKSPACE_SERVICE( workspace_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 ); - - FoobarPanel* self = g_object_new( FOOBAR_TYPE_PANEL, "application", app, NULL ); - self->battery_service = g_object_ref( battery_service ); - self->clock_service = g_object_ref( clock_service ); - self->brightness_service = g_object_ref( brightness_service ); - self->workspace_service = g_object_ref( workspace_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 ); - - // Set the monitor. - - if ( monitor ) - { - self->monitor = g_object_ref( monitor ); - gtk_layer_set_monitor( GTK_WINDOW( self ), self->monitor ); - } - - // Apply the configuration and subscribe to changes. - - FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); - foobar_panel_apply_configuration( self, foobar_configuration_get_panel( config ) ); - self->config_handler_id = g_signal_connect( - self->configuration_service, - "notify::current", - G_CALLBACK( foobar_panel_handle_config_change ), - self ); - - return self; -} - -// -// Apply the panel configuration provided by the configuration service. -// -void foobar_panel_apply_configuration( - FoobarPanel* self, - FoobarPanelConfiguration const* config ) -{ - g_return_if_fail( FOOBAR_IS_PANEL( self ) ); - g_return_if_fail( config != NULL ); - - // Derive the orientation from the panel's position. - - FoobarScreenEdge position = foobar_panel_configuration_get_position( config ); - GtkOrientation orientation; - gchar const* orientation_css_class; - switch ( position ) - { - case FOOBAR_SCREEN_EDGE_LEFT: - case FOOBAR_SCREEN_EDGE_RIGHT: - orientation = GTK_ORIENTATION_VERTICAL; - orientation_css_class = "vertical"; - break; - case FOOBAR_SCREEN_EDGE_TOP: - case FOOBAR_SCREEN_EDGE_BOTTOM: - orientation = GTK_ORIENTATION_HORIZONTAL; - orientation_css_class = "horizontal"; - break; - default: - g_warn_if_reached( ); - return; - } - - // Configure the window. - - gtk_widget_remove_css_class( GTK_WIDGET( self ), "horizontal" ); - gtk_widget_remove_css_class( GTK_WIDGET( self ), "vertical" ); - gtk_widget_add_css_class( GTK_WIDGET( self ), orientation_css_class ); - - gint size = foobar_panel_configuration_get_size( config ); - gtk_window_set_default_size( - GTK_WINDOW( self ), - orientation == GTK_ORIENTATION_VERTICAL ? size : -1, - orientation == GTK_ORIENTATION_HORIZONTAL ? size : -1 ); - - gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_LEFT, position != FOOBAR_SCREEN_EDGE_RIGHT ); - gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_RIGHT, position != FOOBAR_SCREEN_EDGE_LEFT ); - gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, position != FOOBAR_SCREEN_EDGE_BOTTOM ); - gtk_layer_set_anchor( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_BOTTOM, position != FOOBAR_SCREEN_EDGE_TOP ); - - // Configure layout spacing. - - gint margin = foobar_panel_configuration_get_margin( config ); - gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_LEFT, position != FOOBAR_SCREEN_EDGE_RIGHT ? margin : 0 ); - gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_RIGHT, position != FOOBAR_SCREEN_EDGE_LEFT ? margin : 0 ); - gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_TOP, position != FOOBAR_SCREEN_EDGE_BOTTOM ? margin : 0 ); - gtk_layer_set_margin( GTK_WINDOW( self ), GTK_LAYER_SHELL_EDGE_BOTTOM, position != FOOBAR_SCREEN_EDGE_TOP ? margin : 0 ); - - gint padding = foobar_panel_configuration_get_padding( config ); - gtk_widget_set_margin_start( GTK_WIDGET( self->main_layout ), orientation == GTK_ORIENTATION_HORIZONTAL ? padding : 0 ); - gtk_widget_set_margin_end( GTK_WIDGET( self->main_layout ), orientation == GTK_ORIENTATION_HORIZONTAL ? padding : 0 ); - gtk_widget_set_margin_top( GTK_WIDGET( self->main_layout ), orientation == GTK_ORIENTATION_VERTICAL ? padding : 0 ); - gtk_widget_set_margin_bottom( GTK_WIDGET( self->main_layout ), orientation == GTK_ORIENTATION_VERTICAL ? padding : 0 ); - - gint spacing = foobar_panel_configuration_get_spacing( config ); - gtk_box_set_spacing( GTK_BOX(self->start_items), spacing ); - gtk_box_set_spacing( GTK_BOX(self->center_items), spacing ); - gtk_box_set_spacing( GTK_BOX(self->end_items), spacing ); - gtk_widget_set_margin_top( GTK_WIDGET( self->center_items ), orientation == GTK_ORIENTATION_VERTICAL ? spacing : 0 ); - gtk_widget_set_margin_bottom( GTK_WIDGET( self->center_items ), orientation == GTK_ORIENTATION_VERTICAL ? spacing : 0 ); - gtk_widget_set_margin_start( GTK_WIDGET( self->center_items ), orientation == GTK_ORIENTATION_HORIZONTAL ? spacing : 0 ); - gtk_widget_set_margin_end( GTK_WIDGET( self->center_items ), orientation == GTK_ORIENTATION_HORIZONTAL ? spacing : 0 ); - - // Configure the container orientations. - - gtk_orientable_set_orientation( GTK_ORIENTABLE( self->main_layout ), orientation ); - gtk_orientable_set_orientation( GTK_ORIENTABLE( self->start_items ), orientation ); - gtk_orientable_set_orientation( GTK_ORIENTABLE( self->center_items ), orientation ); - gtk_orientable_set_orientation( GTK_ORIENTABLE( self->end_items ), orientation ); - - // Re-create panel items. - - if ( self->item_widgets ) - { - for ( guint i = 0; i < self->item_widgets->len; ++i ) - { - gtk_widget_unparent( g_ptr_array_index( self->item_widgets, i ) ); - } - g_clear_pointer( &self->item_widgets, g_ptr_array_unref ); - } - - gsize items_count; - FoobarPanelItemConfiguration const* const* items = foobar_panel_configuration_get_items( config, &items_count ); - self->item_widgets = g_ptr_array_sized_new( items_count ); - for ( gsize i = 0; i < items_count; ++i ) - { - // Create an item for the specified type and configuration. - - FoobarPanelItemConfiguration const* item = items[i]; - FoobarPanelItem* item_widget; - switch ( foobar_panel_item_configuration_get_kind( item ) ) - { - case FOOBAR_PANEL_ITEM_KIND_ICON: - item_widget = foobar_panel_item_icon_new( item ); - break; - case FOOBAR_PANEL_ITEM_KIND_CLOCK: - item_widget = foobar_panel_item_clock_new( - item, - self->clock_service ); - break; - case FOOBAR_PANEL_ITEM_KIND_WORKSPACES: - item_widget = foobar_panel_item_workspaces_new( - item, - self->monitor, - self->workspace_service ); - break; - case FOOBAR_PANEL_ITEM_KIND_STATUS: - item_widget = foobar_panel_item_status_new( - item, - self->battery_service, - self->brightness_service, - self->audio_service, - self->network_service, - self->bluetooth_service, - self->notification_service ); - break; - default: - g_warn_if_reached( ); - continue; - } - - // Propagate the panel's orientation if the item supports it. - - if ( GTK_IS_ORIENTABLE( item_widget ) ) - { - gtk_orientable_set_orientation( GTK_ORIENTABLE( item_widget ), orientation ); - } - - // Add the item to the correct container, depending on its configured position. - - switch ( foobar_panel_item_configuration_get_position( item ) ) - { - case FOOBAR_PANEL_ITEM_POSITION_START: - gtk_box_append( GTK_BOX( self->start_items ), GTK_WIDGET( item_widget ) ); - break; - case FOOBAR_PANEL_ITEM_POSITION_CENTER: - gtk_box_append( GTK_BOX( self->center_items ), GTK_WIDGET( item_widget ) ); - break; - case FOOBAR_PANEL_ITEM_POSITION_END: - gtk_box_append( GTK_BOX( self->end_items ), GTK_WIDGET( item_widget ) ); - break; - default: - g_warn_if_reached( ); - break; - } - - // Add the item to the pointer array for easier cleanup. - - g_ptr_array_add( self->item_widgets, GTK_WIDGET( item_widget ) ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Signal handler called when the global configuration file has changed. -// -void foobar_panel_handle_config_change( - GObject* object, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)object; - (void)pspec; - FoobarPanel* self = (FoobarPanel*)userdata; - - FoobarConfiguration const* config = foobar_configuration_service_get_current( self->configuration_service ); - foobar_panel_apply_configuration( self, foobar_configuration_get_panel( config ) ); -} diff --git a/src/panel.h b/src/panel.h deleted file mode 100644 index e64a7d7..0000000 --- a/src/panel.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include -#include "services/configuration-service.h" -#include "services/battery-service.h" -#include "services/clock-service.h" -#include "services/brightness-service.h" -#include "services/workspace-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 foobar_panel_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarPanel, foobar_panel, FOOBAR, PANEL, GtkApplicationWindow ) - -FoobarPanel* foobar_panel_new ( GtkApplication* app, - GdkMonitor* monitor, - FoobarBatteryService* battery_service, - FoobarClockService* clock_service, - FoobarBrightnessService* brightness_service, - FoobarWorkspaceService* workspace_service, - FoobarAudioService* audio_service, - FoobarNetworkService* network_service, - FoobarBluetoothService* bluetooth_service, - FoobarNotificationService* notification_service, - FoobarConfigurationService* configuration_service ); -void foobar_panel_apply_configuration( FoobarPanel* self, - FoobarPanelConfiguration const* config ); - -G_END_DECLS diff --git a/src/services/application-service.c b/src/services/application-service.c deleted file mode 100644 index 42835ac..0000000 --- a/src/services/application-service.c +++ /dev/null @@ -1,746 +0,0 @@ -#include "services/application-service.h" -#include "launcher-item.h" -#include "utils.h" -#include -#include -#include -#include - -// -// FoobarApplicationItem: -// -// Represents a single application/desktop file. -// - -struct _FoobarApplicationItem -{ - GObject parent_instance; - FoobarApplicationService* service; - GAppInfo* info; -}; - -enum -{ - APP_PROP_ID = 1, - APP_PROP_EXECUTABLE, - APP_PROP_CATEGORIES, - APP_PROP_FREQUENCY, - APP_PROP_TITLE, - APP_PROP_DESCRIPTION, - APP_PROP_ICON, - N_APP_PROPS, -}; - -static GParamSpec* app_props[N_APP_PROPS] = { 0 }; - -static void foobar_application_item_class_init ( FoobarApplicationItemClass* klass ); -static void foobar_application_item_launcher_item_interface_init( FoobarLauncherItemInterface* iface ); -static void foobar_application_item_init ( FoobarApplicationItem* self ); -static void foobar_application_item_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_application_item_finalize ( GObject* object ); -static FoobarApplicationItem* foobar_application_item_new ( FoobarApplicationService* service ); -static gchar const* foobar_application_item_get_title ( FoobarLauncherItem* self ); -static gchar const* foobar_application_item_get_description ( FoobarLauncherItem* self ); -static GIcon* foobar_application_item_get_icon ( FoobarLauncherItem* self ); -static void foobar_application_item_activate ( FoobarLauncherItem* self ); -static void foobar_application_item_set_info ( FoobarApplicationItem* self, - GAppInfo* value ); - -G_DEFINE_FINAL_TYPE_WITH_CODE( - FoobarApplicationItem, - foobar_application_item, - G_TYPE_OBJECT, - G_IMPLEMENT_INTERFACE( FOOBAR_TYPE_LAUNCHER_ITEM, foobar_application_item_launcher_item_interface_init ) ) - -// -// FoobarApplicationService: -// -// Service providing a list of all installed applications. This implemented using GLib's AppInfo API. -// - -struct _FoobarApplicationService -{ - GObject parent_instance; - GListStore* items; - GtkSortListModel* sorted_items; - GAppInfoMonitor* monitor; - GHashTable* frequencies; // only modified on the main thread, but possibly read on a background thread - GMutex frequencies_mutex; - GMutex write_cache_mutex; - gchar* cache_path; - gulong changed_handler_id; -}; - -enum -{ - PROP_ITEMS = 1, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_application_service_class_init ( FoobarApplicationServiceClass* klass ); -static void foobar_application_service_init ( FoobarApplicationService* self ); -static void foobar_application_service_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_application_service_finalize ( GObject* object ); -static void foobar_application_service_handle_changed ( GAppInfoMonitor* monitor, - gpointer userdata ); -static void foobar_application_service_update ( FoobarApplicationService* self ); -static void foobar_application_service_read_cache ( FoobarApplicationService* self ); -static void foobar_application_service_read_cache_foreach_cb ( JsonObject* object, - gchar const* member_name, - JsonNode* member_node, - gpointer userdata ); -static void foobar_application_service_write_cache ( FoobarApplicationService* self ); -static void foobar_application_service_write_cache_cb ( GObject* object, - GAsyncResult* result, - gpointer userdata ); -static void foobar_application_service_write_cache_async ( FoobarApplicationService* self, - GCancellable* cancellable, - GAsyncReadyCallback callback, - gpointer userdata ); -static gboolean foobar_application_service_write_cache_finish ( FoobarApplicationService* self, - GAsyncResult* result, - GError** error ); -static void foobar_application_service_write_cache_thread ( GTask* task, - gpointer source_object, - gpointer task_data, - GCancellable* cancellable ); -static void foobar_application_service_write_cache_foreach_cb( gpointer key, - gpointer value, - gpointer userdata ); -static gint foobar_application_service_sort_func ( gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarApplicationService, foobar_application_service, G_TYPE_OBJECT ) - -// --------------------------------------------------------------------------------------------------------------------- -// Item Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for application items. -// -void foobar_application_item_class_init( FoobarApplicationItemClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_application_item_get_property; - object_klass->finalize = foobar_application_item_finalize; - - gpointer launcher_item_iface = g_type_default_interface_peek( FOOBAR_TYPE_LAUNCHER_ITEM ); - app_props[APP_PROP_ID] = g_param_spec_string( - "id", - "ID", - "Application identifier string.", - NULL, - G_PARAM_READABLE ); - app_props[APP_PROP_EXECUTABLE] = g_param_spec_string( - "executable", - "Executable", - "The executable name of the application.", - NULL, - G_PARAM_READABLE ); - app_props[APP_PROP_CATEGORIES] = g_param_spec_string( - "categories", - "Categories", - "Semicolon-separated list of application categories.", - NULL, - G_PARAM_READABLE ); - app_props[APP_PROP_FREQUENCY] = g_param_spec_int64( - "frequency", - "Frequency", - "The number of times the application was opened.", - 0, - INT64_MAX, - 0, - G_PARAM_READABLE ); - app_props[APP_PROP_TITLE] = g_param_spec_override( - "title", - g_object_interface_find_property( launcher_item_iface, "title" ) ); - app_props[APP_PROP_DESCRIPTION] = g_param_spec_override( - "description", - g_object_interface_find_property( launcher_item_iface, "description" ) ); - app_props[APP_PROP_ICON] = g_param_spec_override( - "icon", - g_object_interface_find_property( launcher_item_iface, "icon" ) ); - g_object_class_install_properties( object_klass, N_APP_PROPS, app_props ); -} - -// -// Static initialization of the FoobarLauncherItem interface. -// -void foobar_application_item_launcher_item_interface_init( FoobarLauncherItemInterface* iface ) -{ - iface->get_title = foobar_application_item_get_title; - iface->get_description = foobar_application_item_get_description; - iface->get_icon = foobar_application_item_get_icon; - iface->activate = foobar_application_item_activate; -} - -// -// Instance initialization for application items. -// -void foobar_application_item_init( FoobarApplicationItem* self ) -{ - (void)self; -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_application_item_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarApplicationItem* self = (FoobarApplicationItem*)object; - - switch ( prop_id ) - { - case APP_PROP_ID: - g_value_set_string( value, foobar_application_item_get_id( self ) ); - break; - case APP_PROP_EXECUTABLE: - g_value_set_string( value, foobar_application_item_get_executable( self ) ); - break; - case APP_PROP_CATEGORIES: - g_value_set_string( value, foobar_application_item_get_categories( self ) ); - break; - case APP_PROP_FREQUENCY: - g_value_set_int64( value, foobar_application_item_get_frequency( self ) ); - break; - case APP_PROP_TITLE: - g_value_set_string( value, foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM ( self ) ) ); - break; - case APP_PROP_DESCRIPTION: - g_value_set_string( value, foobar_launcher_item_get_description( FOOBAR_LAUNCHER_ITEM( self ) ) ); - break; - case APP_PROP_ICON: - g_value_set_object( value, foobar_launcher_item_get_icon( FOOBAR_LAUNCHER_ITEM( self ) ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for application items. -// -void foobar_application_item_finalize( GObject* object ) -{ - FoobarApplicationItem* self = (FoobarApplicationItem*)object; - - g_clear_object( &self->info ); - - G_OBJECT_CLASS( foobar_application_item_parent_class )->finalize( object ); -} - -// -// Create a new application item belonging to the parent service (captured as an unowned reference). -// -FoobarApplicationItem* foobar_application_item_new( FoobarApplicationService* service ) -{ - FoobarApplicationItem* self = g_object_new( FOOBAR_TYPE_APPLICATION_ITEM, NULL ); - self->service = service; - return self; -} - -// -// Get the application's unique textual identifier. -// -gchar const* foobar_application_item_get_id( FoobarApplicationItem* self ) -{ - g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL ); - return self->info ? g_app_info_get_id( self->info ) : NULL; -} - -// -// Get the application's executable name. -// -gchar const* foobar_application_item_get_executable( FoobarApplicationItem* self ) -{ - g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL ); - return self->info ? g_app_info_get_executable( self->info ) : NULL; -} - -// -// Get the semicolon-separated list of categories for this application. -// -gchar const* foobar_application_item_get_categories( FoobarApplicationItem* self ) -{ - g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), NULL ); - return G_IS_DESKTOP_APP_INFO( self->info ) - ? g_desktop_app_info_get_categories( G_DESKTOP_APP_INFO( self->info ) ) - : NULL; -} - -// -// Get the number of times this application was opened using the launcher. -// -gint64 foobar_application_item_get_frequency( FoobarApplicationItem* self ) -{ - g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), 0 ); - - if ( !self->service ) { return 0; } - gpointer value = g_hash_table_lookup( self->service->frequencies, foobar_application_item_get_id( self ) ); - return GPOINTER_TO_SIZE( value ); -} - -// -// Get the application's human-readable name. -// -gchar const* foobar_application_item_get_title( FoobarLauncherItem* item ) -{ - FoobarApplicationItem* self = ( FoobarApplicationItem* )item; - return self->info ? g_app_info_get_display_name( self->info ) : NULL; -} - -// -// Get an optional short description for the application. -// -gchar const* foobar_application_item_get_description( FoobarLauncherItem* item ) -{ - FoobarApplicationItem* self = ( FoobarApplicationItem* )item; - return self->info ? g_app_info_get_description( self->info ) : NULL; -} - -// -// Get an icon representing the application. -// -GIcon* foobar_application_item_get_icon( FoobarLauncherItem* item ) -{ - FoobarApplicationItem* self = ( FoobarApplicationItem* )item; - return self->info ? g_app_info_get_icon( self->info ) : NULL; -} - -// -// Launch the application, increasing its frequency by one. -// -void foobar_application_item_activate( FoobarLauncherItem* item ) -{ - FoobarApplicationItem* self = ( FoobarApplicationItem* )item; - - if ( !self->info ) { return; } - - g_autoptr( GError ) error = NULL; - if ( !g_app_info_launch( self->info, NULL, NULL, &error ) ) - { - g_warning( "Unable to launch application: %s", error->message ); - } - - gchar const* id = foobar_application_item_get_id( self ); - if ( !self->service || !id ) { return; } - - gpointer value = g_hash_table_lookup( self->service->frequencies, id ); - value = GSIZE_TO_POINTER( GPOINTER_TO_SIZE( value ) + 1 ); - g_mutex_lock( &self->service->frequencies_mutex ); - g_hash_table_insert( self->service->frequencies, g_strdup( id ), value ); - g_mutex_unlock( &self->service->frequencies_mutex ); - - g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_FREQUENCY] ); - gtk_sorter_changed( gtk_sort_list_model_get_sorter( self->service->sorted_items ), GTK_SORTER_CHANGE_DIFFERENT ); - foobar_application_service_write_cache( self->service ); -} - -// -// Update the backing GAppInfo object for the application. -// -void foobar_application_item_set_info( - FoobarApplicationItem* self, - GAppInfo* value ) -{ - g_return_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ) ); - - if ( self->info != value ) - { - g_clear_object( &self->info ); - - if ( value ) { self->info = g_object_ref( value ); } - - g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_ID] ); - g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_TITLE] ); - g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_DESCRIPTION] ); - g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_EXECUTABLE] ); - g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_ICON] ); - g_object_notify_by_pspec( G_OBJECT( self ), app_props[APP_PROP_FREQUENCY] ); - } -} - -// -// Match the item against the given search terms. -// -gboolean foobar_application_item_match( - FoobarApplicationItem* self, - gchar const* const* terms ) -{ - g_return_val_if_fail( FOOBAR_IS_APPLICATION_ITEM( self ), FALSE ); - g_return_val_if_fail( terms != NULL, FALSE ); - - for ( gchar const* const* it = terms; *it; ++it ) - { - gchar const* term = *it; - gchar const* name = foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM( self ) ); - gchar const* description = foobar_launcher_item_get_description( FOOBAR_LAUNCHER_ITEM( self ) ); - gchar const* executable = foobar_application_item_get_executable( self ); - gchar const* categories = foobar_application_item_get_categories( self ); - gchar const* id = foobar_application_item_get_id( self ); - if ( name && strcasestr( name, term ) != NULL ) { continue; } - if ( description && strcasestr( description, term ) != NULL ) { continue; } - if ( executable && strcasestr( executable, term ) != NULL ) { continue; } - if ( categories && strcasestr( categories, term ) != NULL ) { continue; } - if ( id && strcasestr( id, term ) != NULL ) { continue; } - - return FALSE; - } - - return TRUE; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Service Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the application service. -// -void foobar_application_service_class_init( FoobarApplicationServiceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_application_service_get_property; - object_klass->finalize = foobar_application_service_finalize; - - props[PROP_ITEMS] = g_param_spec_object( - "items", - "Items", - "Application items, sorted by their frequency.", - G_TYPE_LIST_MODEL, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for the application service. -// -void foobar_application_service_init( FoobarApplicationService* self ) -{ - self->items = g_list_store_new( FOOBAR_TYPE_APPLICATION_ITEM ); - - GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_application_service_sort_func, NULL, NULL ); - self->sorted_items = gtk_sort_list_model_new( G_LIST_MODEL( g_object_ref( self->items ) ), GTK_SORTER( sorter ) ); - - self->cache_path = foobar_get_cache_path( "application-frequencies.json" ); - self->frequencies = g_hash_table_new_full( g_str_hash, g_str_equal, g_free, NULL ); - g_mutex_init( &self->frequencies_mutex ); - g_mutex_init( &self->write_cache_mutex ); - - foobar_application_service_read_cache( self ); - foobar_application_service_update( self ); - - self->monitor = g_app_info_monitor_get( ); - self->changed_handler_id = g_signal_connect( - self->monitor, - "changed", - G_CALLBACK( foobar_application_service_handle_changed ), - self ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_application_service_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarApplicationService* self = (FoobarApplicationService*)object; - - switch ( prop_id ) - { - case PROP_ITEMS: - g_value_set_object( value, foobar_application_service_get_items( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for the application service. -// -void foobar_application_service_finalize( GObject* object ) -{ - FoobarApplicationService* self = (FoobarApplicationService*)object; - - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->items ) ); ++i ) - { - FoobarApplicationItem* item = g_list_model_get_item( G_LIST_MODEL( self->items ), i ); - item->service = NULL; - g_object_notify_by_pspec( G_OBJECT( item ), app_props[APP_PROP_FREQUENCY] ); - } - g_clear_signal_handler( &self->changed_handler_id, self->monitor ); - g_clear_object( &self->sorted_items ); - g_clear_object( &self->items ); - g_clear_object( &self->monitor ); - g_clear_pointer( &self->frequencies, g_hash_table_unref ); - g_clear_pointer( &self->cache_path, g_free ); - - g_mutex_clear( &self->frequencies_mutex ); - g_mutex_clear( &self->write_cache_mutex ); - - G_OBJECT_CLASS( foobar_application_service_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new application service instance. -// -FoobarApplicationService* foobar_application_service_new( void ) -{ - return g_object_new( FOOBAR_TYPE_APPLICATION_SERVICE, NULL ); -} - -// -// Get a sorted list of all available applications. -// -GListModel* foobar_application_service_get_items( FoobarApplicationService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_APPLICATION_SERVICE( self ), NULL ); - - return G_LIST_MODEL( self->sorted_items ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called by GLib when the list of applications has changed. -// -void foobar_application_service_handle_changed( - GAppInfoMonitor* monitor, - gpointer userdata ) -{ - (void)monitor; - FoobarApplicationService* self = (FoobarApplicationService*)userdata; - - foobar_application_service_update( self ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Methods -// --------------------------------------------------------------------------------------------------------------------- - -// -// Reload the list of applications. -// -void foobar_application_service_update( FoobarApplicationService* self ) -{ - g_list_store_remove_all( self->items ); - - g_autolist( GAppInfo ) info = g_app_info_get_all( ); - for ( GList* it = info; it; it = it->next ) - { - if ( g_app_info_should_show( it->data ) ) - { - g_autoptr( FoobarApplicationItem ) item = foobar_application_item_new( self ); - foobar_application_item_set_info( item, it->data ); - g_list_store_append( self->items, item ); - } - } -} - -// -// Synchronously read the frequency cache file at self->cache_path, populating self->frequencies. -// -void foobar_application_service_read_cache( FoobarApplicationService* self ) -{ - g_autoptr( GError ) error = NULL; - - if ( self->cache_path && g_file_test( self->cache_path, G_FILE_TEST_EXISTS ) ) - { - g_autoptr( JsonParser ) parser = json_parser_new( ); - if ( !json_parser_load_from_mapped_file( parser, self->cache_path, &error ) ) - { - g_warning( "Unable to read cached application frequencies: %s", error->message ); - return; - } - - JsonNode* root_node = json_parser_get_root( parser ); - JsonObject* root_object = json_node_get_object( root_node ); - json_object_foreach_member( root_object, foobar_application_service_read_cache_foreach_cb, self ); - } -} - -// -// Callback used by foobar_application_service_read_cache to read a single entry. -// -void foobar_application_service_read_cache_foreach_cb( - JsonObject* object, - gchar const* member_name, - JsonNode* member_node, - gpointer userdata ) -{ - (void)object; - FoobarApplicationService* self = (FoobarApplicationService*)userdata; - - gsize frequency = json_node_get_int( member_node ); - g_hash_table_insert( self->frequencies, g_strdup( member_name ), GSIZE_TO_POINTER( frequency ) ); -} - -// -// Asynchronously start writing the frequency cache file at self->cache_path. -// -void foobar_application_service_write_cache( FoobarApplicationService* self ) -{ - foobar_application_service_write_cache_async( self, NULL, foobar_application_service_write_cache_cb, NULL ); -} - -// -// Callback invoked when the frequency cache was written successfully or failed. -// -void foobar_application_service_write_cache_cb( - GObject* object, - GAsyncResult* result, - gpointer userdata ) -{ - (void)userdata; - FoobarApplicationService* self = (FoobarApplicationService*)object; - - g_autoptr( GError ) error = NULL; - if ( !foobar_application_service_write_cache_finish( self, result, &error ) ) - { - g_warning( "Unable to write cached application frequencies: %s", error->message ); - } -} - -// -// Asynchronously write the frequency cache file at self->cache_path. -// -void foobar_application_service_write_cache_async( - FoobarApplicationService* self, - GCancellable* cancellable, - GAsyncReadyCallback callback, - gpointer userdata ) -{ - g_autoptr( GTask ) task = g_task_new( self, cancellable, callback, userdata ); - g_task_set_name( task, "write-application-cache" ); - g_task_run_in_thread( task, foobar_application_service_write_cache_thread ); -} - -// -// Get the asynchronous result for writing the frequency cache file, returning TRUE on success, or FALSE on error. -// -gboolean foobar_application_service_write_cache_finish( - FoobarApplicationService* self, - GAsyncResult* result, - GError** error ) -{ - (void)self; - - return g_task_propagate_boolean( G_TASK( result ), error ); -} - -// -// Task implementation for foobar_application_service_write_cache_async, invoked on a background thread. -// -void foobar_application_service_write_cache_thread( - GTask* task, - gpointer source_object, - gpointer task_data, - GCancellable* cancellable ) -{ - (void)cancellable; - (void)task_data; - FoobarApplicationService* self = (FoobarApplicationService*)source_object; - - if ( !self->cache_path ) - { - g_task_return_boolean( task, TRUE ); - return; - } - - g_autoptr( GError ) error = NULL; - - g_autoptr( JsonBuilder ) builder = json_builder_new( ); - json_builder_begin_object( builder ); - - g_mutex_lock( &self->frequencies_mutex ); - g_hash_table_foreach( self->frequencies, foobar_application_service_write_cache_foreach_cb, builder ); - g_mutex_unlock( &self->frequencies_mutex ); - - json_builder_end_object( builder ); - - g_autoptr( JsonNode ) root_node = json_builder_get_root( builder ); - g_autoptr( JsonGenerator ) generator = json_generator_new( ); - json_generator_set_root( generator, root_node ); - - g_mutex_lock( &self->write_cache_mutex ); - gboolean success = json_generator_to_file( generator, self->cache_path, &error ); - g_mutex_unlock( &self->write_cache_mutex ); - if ( !success ) - { - g_task_return_error( task, g_steal_pointer( &error ) ); - return; - } - - g_task_return_boolean( task, TRUE ); -} - -// -// Callback used by foobar_application_service_write_cache_thread to build a single entry. -// -void foobar_application_service_write_cache_foreach_cb( - gpointer key, - gpointer value, - gpointer userdata ) -{ - JsonBuilder* builder = (JsonBuilder*)userdata; - - json_builder_set_member_name( builder, key ); - json_builder_add_int_value( builder, GPOINTER_TO_SIZE( value ) ); -} - -// -// Sorting callback for application items. Items are sorted based on: -// 1. frequency (descending) -// 2. name (ascending) -// 3. id (ascending) -// -gint foobar_application_service_sort_func( - gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ) -{ - (void)userdata; - - FoobarApplicationItem* app_a = (FoobarApplicationItem*)item_a; - FoobarApplicationItem* app_b = (FoobarApplicationItem*)item_b; - - gint64 freq_a = foobar_application_item_get_frequency( app_a ); - gint64 freq_b = foobar_application_item_get_frequency( app_b ); - if ( freq_a > freq_b ) { return -1; } - if ( freq_a < freq_b ) { return 1; } - - gchar const* name_a = foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM( app_a ) ); - gchar const* name_b = foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM( app_b ) ); - gint name_res = g_strcmp0( name_a, name_b ); - if ( name_res ) { return name_res; } - - gchar const* id_a = foobar_application_item_get_id( app_a ); - gchar const* id_b = foobar_application_item_get_id( app_b ); - return g_strcmp0( id_a, id_b ); -} diff --git a/src/services/application-service.h b/src/services/application-service.h deleted file mode 100644 index dd043fc..0000000 --- a/src/services/application-service.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_APPLICATION_ITEM foobar_application_item_get_type( ) -#define FOOBAR_TYPE_APPLICATION_SERVICE foobar_application_service_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarApplicationItem, foobar_application_item, FOOBAR, APPLICATION_ITEM, GObject ) - -gchar const* foobar_application_item_get_id ( FoobarApplicationItem* self ); -gchar const* foobar_application_item_get_executable ( FoobarApplicationItem* self ); -gchar const* foobar_application_item_get_categories ( FoobarApplicationItem* self ); -gint64 foobar_application_item_get_frequency ( FoobarApplicationItem* self ); -gboolean foobar_application_item_match ( FoobarApplicationItem* self, - gchar const* const* terms ); - -G_DECLARE_FINAL_TYPE( FoobarApplicationService, foobar_application_service, FOOBAR, APPLICATION_SERVICE, GObject ) - -FoobarApplicationService* foobar_application_service_new ( void ); -GListModel* foobar_application_service_get_items( FoobarApplicationService* self ); - -G_END_DECLS diff --git a/src/services/audio-service.c b/src/services/audio-service.c deleted file mode 100644 index 06345f2..0000000 --- a/src/services/audio-service.c +++ /dev/null @@ -1,930 +0,0 @@ -#include "services/audio-service.h" -#include -#include -#include -#include -#include -#include - -#define DEFAULT_VOLUME 25 - -// -// FoobarAudioDeviceKind: -// -// The type of an audio device (either speaker/output or microphone/input). -// - -G_DEFINE_ENUM_TYPE( - FoobarAudioDeviceKind, - foobar_audio_device_kind, - G_DEFINE_ENUM_VALUE( FOOBAR_AUDIO_DEVICE_INPUT, "input" ), - G_DEFINE_ENUM_VALUE( FOOBAR_AUDIO_DEVICE_OUTPUT, "output" ) ) - -// -// FoobarAudioDevice: -// -// Representation of a real input/output device or an abstract device (like the default device which only acts as a -// proxy for another device). -// - -struct _FoobarAudioDevice -{ - GObject parent_instance; - FoobarAudioService* service; - FoobarAudioDeviceKind kind; - GvcMixerStream* stream; - gboolean is_default; - gulong notify_handler_id; -}; - -enum -{ - DEVICE_PROP_KIND = 1, - DEVICE_PROP_ID, - DEVICE_PROP_NAME, - DEVICE_PROP_DESCRIPTION, - DEVICE_PROP_VOLUME, - DEVICE_PROP_IS_MUTED, - DEVICE_PROP_IS_DEFAULT, - DEVICE_PROP_IS_AVAILABLE, - N_DEVICE_PROPS, -}; - -static GParamSpec* device_props[N_DEVICE_PROPS] = { 0 }; - -static void foobar_audio_device_class_init ( FoobarAudioDeviceClass* klass ); -static void foobar_audio_device_init ( FoobarAudioDevice* self ); -static void foobar_audio_device_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_audio_device_set_property ( GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ); -static void foobar_audio_device_finalize ( GObject* object ); -static FoobarAudioDevice* foobar_audio_device_new ( FoobarAudioService* service, - FoobarAudioDeviceKind kind ); -static void foobar_audio_device_set_stream ( FoobarAudioDevice* self, - GvcMixerStream* value ); -static void foobar_audio_device_set_default ( FoobarAudioDevice* self, - gboolean value ); -static void foobar_audio_device_handle_notify( GObject* object, - GParamSpec* pspec, - gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarAudioDevice, foobar_audio_device, G_TYPE_OBJECT ) - -// -// FoobarAudioService: -// -// Service managing the available audio input/output devices. This is implemented using the Gnome Volume Control -// library. -// - -struct _FoobarAudioService -{ - GObject parent_instance; - GvcMixerControl* control; - GListStore* devices; - GtkSortListModel* sorted_devices; - GtkFilterListModel* inputs; - GtkFilterListModel* outputs; - FoobarAudioDevice* default_input; - FoobarAudioDevice* default_output; - gulong default_sink_handler_id; - gulong default_source_handler_id; - gulong stream_added_handler_id; - gulong stream_removed_handler_id; -}; - -enum -{ - PROP_INPUTS = 1, - PROP_OUTPUTS, - PROP_DEFAULT_INPUT, - PROP_DEFAULT_OUTPUT, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_audio_service_class_init ( FoobarAudioServiceClass* klass ); -static void foobar_audio_service_init ( FoobarAudioService* self ); -static void foobar_audio_service_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_audio_service_finalize ( GObject* object ); -static void foobar_audio_service_handle_default_sink_changed ( GvcMixerControl* control, - guint id, - gpointer userdata ); -static void foobar_audio_service_handle_default_source_changed( GvcMixerControl* control, - guint id, - gpointer userdata ); -static void foobar_audio_service_handle_stream_added ( GvcMixerControl* control, - guint id, - gpointer userdata ); -static void foobar_audio_service_handle_stream_removed ( GvcMixerControl* control, - guint id, - gpointer userdata ); -static gboolean foobar_audio_service_identify_stream ( GvcMixerStream* stream, - FoobarAudioDeviceKind* out_kind ); -static void foobar_audio_service_update_default ( GListModel* list, - guint id ); -static gint foobar_audio_service_sort_func ( gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ); -static gboolean foobar_audio_service_input_filter_func ( gpointer item, - gpointer userdata ); -static gboolean foobar_audio_service_output_filter_func ( gpointer item, - gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarAudioService, foobar_audio_service, G_TYPE_OBJECT ) - -// --------------------------------------------------------------------------------------------------------------------- -// Device -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for audio devices. -// -void foobar_audio_device_class_init( FoobarAudioDeviceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_audio_device_get_property; - object_klass->set_property = foobar_audio_device_set_property; - object_klass->finalize = foobar_audio_device_finalize; - - device_props[DEVICE_PROP_KIND] = g_param_spec_enum( - "kind", - "Kind", - "The kind of audio device.", - FOOBAR_TYPE_AUDIO_DEVICE_KIND, - FOOBAR_AUDIO_DEVICE_INPUT, - G_PARAM_READABLE ); - device_props[DEVICE_PROP_ID] = g_param_spec_uint( - "id", - "ID", - "Current numeric ID of the device (may change over time).", - 0, - UINT_MAX, - UINT_MAX, - G_PARAM_READABLE ); - device_props[DEVICE_PROP_NAME] = g_param_spec_string( - "name", - "Name", - "Current name of the device.", - NULL, - G_PARAM_READABLE ); - device_props[DEVICE_PROP_DESCRIPTION] = g_param_spec_string( - "description", - "Description", - "Short textual description of the device.", - NULL, - G_PARAM_READABLE ); - device_props[DEVICE_PROP_VOLUME] = g_param_spec_int( - "volume", - "Volume", - "Current volume in percent.", - 0, - 100, - 0, - G_PARAM_READWRITE ); - device_props[DEVICE_PROP_IS_MUTED] = g_param_spec_int( - "is-muted", - "Is Muted", - "Indicates whether the device is currently muted (i.e. the volume is 0).", - 0, - 100, - 0, - G_PARAM_READWRITE ); - device_props[DEVICE_PROP_IS_DEFAULT] = g_param_spec_boolean( - "is-default", - "Is Default", - "Indicates whether the device is currently also a default device (either microphone or speaker).", - FALSE, - G_PARAM_READABLE ); - device_props[DEVICE_PROP_IS_AVAILABLE] = g_param_spec_boolean( - "is-available", - "Is Available", - "Indicates whether the device is currently available to use/plugged in.", - FALSE, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_DEVICE_PROPS, device_props ); -} - -// -// Instance initialization for audio devices. -// -void foobar_audio_device_init( FoobarAudioDevice* self ) -{ - (void)self; -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_audio_device_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarAudioDevice* self = (FoobarAudioDevice*)object; - - switch ( prop_id ) - { - case DEVICE_PROP_KIND: - g_value_set_enum( value, foobar_audio_device_get_kind( self ) ); - break; - case DEVICE_PROP_ID: - g_value_set_uint( value, foobar_audio_device_get_id( self ) ); - break; - case DEVICE_PROP_NAME: - g_value_set_string( value, foobar_audio_device_get_name( self ) ); - break; - case DEVICE_PROP_DESCRIPTION: - g_value_set_string( value, foobar_audio_device_get_description( self ) ); - break; - case DEVICE_PROP_VOLUME: - g_value_set_int( value, foobar_audio_device_get_volume( self ) ); - break; - case DEVICE_PROP_IS_MUTED: - g_value_set_boolean( value, foobar_audio_device_is_muted( self ) ); - break; - case DEVICE_PROP_IS_DEFAULT: - g_value_set_boolean( value, foobar_audio_device_is_default( self ) ); - break; - case DEVICE_PROP_IS_AVAILABLE: - g_value_set_boolean( value, foobar_audio_device_is_available( 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_audio_device_set_property( - GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ) -{ - FoobarAudioDevice* self = (FoobarAudioDevice*)object; - - switch ( prop_id ) - { - case DEVICE_PROP_VOLUME: - foobar_audio_device_set_volume( self, g_value_get_int( value ) ); - break; - case DEVICE_PROP_IS_MUTED: - foobar_audio_device_set_muted( self, g_value_get_boolean( value ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for audio devices. -// -void foobar_audio_device_finalize( GObject* object ) -{ - FoobarAudioDevice* self = (FoobarAudioDevice*)object; - - g_clear_signal_handler( &self->notify_handler_id, self->stream ); - g_clear_object( &self->stream ); - - G_OBJECT_CLASS( foobar_audio_device_parent_class )->finalize( object ); -} - -// -// Create a new audio device object with the specified parent service (captured as an unowned reference). -// -FoobarAudioDevice* foobar_audio_device_new( - FoobarAudioService* service, - FoobarAudioDeviceKind kind ) -{ - FoobarAudioDevice* self = g_object_new( FOOBAR_TYPE_AUDIO_DEVICE, NULL ); - self->service = service; - self->kind = kind; - return self; -} - -// -// Get the type of this device. -// -FoobarAudioDeviceKind foobar_audio_device_get_kind( FoobarAudioDevice* self ) -{ - g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), 0 ); - return self->kind; -} - -// -// Get a numeric identifier for the device provided by the system. -// -guint foobar_audio_device_get_id( FoobarAudioDevice* self ) -{ - g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), 0 ); - return self->stream ? gvc_mixer_stream_get_id( self->stream ) : UINT_MAX; -} - -// -// Get the internal name for the device. This should not be used in the UI. -// -gchar const* foobar_audio_device_get_name( FoobarAudioDevice* self ) -{ - g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), NULL ); - return self->stream ? gvc_mixer_stream_get_name( self->stream ) : NULL; -} - -// -// Get a human-readable description for the device. -// -gchar const* foobar_audio_device_get_description( FoobarAudioDevice* self ) -{ - g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), NULL ); - return self->stream ? gvc_mixer_stream_get_description( self->stream ) : NULL; -} - -// -// Get the current volume as a percentage value. If muted, this is 0. -// -gint foobar_audio_device_get_volume( FoobarAudioDevice* self ) -{ - g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), 0 ); - - if ( !self->service ) { return 0; } - if ( self->stream && gvc_mixer_stream_get_is_muted( self->stream ) ) { return 0; } - gdouble max_volume = gvc_mixer_control_get_vol_max_norm( self->service->control ); - gdouble cur_volume = self->stream ? gvc_mixer_stream_get_volume( self->stream ) : 0; - gint percentage = (gint)round( 100. * cur_volume / max_volume ); - return CLAMP( percentage, 0, 100 ); -} - -// -// Check whether the device is currently muted, i.e. its volume is 0. -// -gboolean foobar_audio_device_is_muted( FoobarAudioDevice* self ) -{ - g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), FALSE ); - return foobar_audio_device_get_volume( self ) == 0; -} - -// -// Check whether this device is the system-default for its kind. -// -// This will always return TRUE for the static default device instance. -// -gboolean foobar_audio_device_is_default( FoobarAudioDevice* self ) -{ - g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), FALSE ); - return self->is_default; -} - -// -// Check whether the device is currently available. -// -gboolean foobar_audio_device_is_available( FoobarAudioDevice* self ) -{ - g_return_val_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ), FALSE ); - return self->stream != NULL; -} - -// -// Update the stream backing this device object. -// -void foobar_audio_device_set_stream( - FoobarAudioDevice* self, - GvcMixerStream* value ) -{ - g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) ); - - if ( self->stream != value ) - { - g_clear_signal_handler( &self->notify_handler_id, self->stream ); - g_clear_object( &self->stream ); - - if ( value ) - { - self->stream = g_object_ref( value ); - self->notify_handler_id = g_signal_connect( - self->stream, - "notify", - G_CALLBACK( foobar_audio_device_handle_notify ), - self ); - } - - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_ID] ); - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_NAME] ); - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_DESCRIPTION] ); - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_VOLUME] ); - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_MUTED] ); - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_AVAILABLE] ); - } -} - -// -// Update the device's "default" flag. This does not actually make it the default device. -// -void foobar_audio_device_set_default( - FoobarAudioDevice* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) ); - - value = !!value; - if ( self->is_default != value ) - { - self->is_default = value; - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_DEFAULT] ); - } -} - -// -// Update the device's volume as a percentage value. -// -void foobar_audio_device_set_volume( - FoobarAudioDevice* self, - gint value ) -{ - g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) ); - - if ( !self->service || !self->stream ) { return; } - - value = CLAMP( value, 0, 100 ); - if ( foobar_audio_device_get_volume( self ) != value ) - { - gdouble max_volume = gvc_mixer_control_get_vol_max_norm( self->service->control ); - gdouble new_volume = value * max_volume / 100.; - gvc_mixer_stream_set_volume( self->stream, (guint32)new_volume ); - gvc_mixer_stream_push_volume( self->stream ); - gvc_mixer_stream_set_is_muted( self->stream, value == 0 ); - gvc_mixer_stream_change_is_muted( self->stream, value == 0 ); - } -} - -// -// Update the device's "muted" state. -// -// If the device was previously muted, the volume is set to either the previous volume or to a default value. -// -void foobar_audio_device_set_muted( - FoobarAudioDevice* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) ); - - if ( foobar_audio_device_is_muted( self ) != value ) - { - if ( self->stream ) - { - gvc_mixer_stream_set_is_muted( self->stream, value ); - gvc_mixer_stream_change_is_muted( self->stream, value ); - } - - if ( !value && foobar_audio_device_get_volume( self ) == 0 ) - { - foobar_audio_device_set_volume( self, DEFAULT_VOLUME ); - } - } -} - -// -// Let this device become default one for its kind. -// -void foobar_audio_device_make_default( FoobarAudioDevice* self ) -{ - g_return_if_fail( FOOBAR_IS_AUDIO_DEVICE( self ) ); - - if ( !self->service || !self->stream ) { return; } - - if ( !foobar_audio_device_is_default( self ) ) - { - switch ( self->kind ) - { - case FOOBAR_AUDIO_DEVICE_INPUT: - gvc_mixer_control_set_default_source( self->service->control, self->stream ); - break; - case FOOBAR_AUDIO_DEVICE_OUTPUT: - gvc_mixer_control_set_default_sink( self->service->control, self->stream ); - break; - default: - break; - } - } -} - -// -// Called by the underlying stream when one of its properties changes. -// -void foobar_audio_device_handle_notify( - GObject* object, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)object; - FoobarAudioDevice* self = (FoobarAudioDevice*)userdata; - - gchar const* property = g_param_spec_get_name( pspec ); - if ( !g_strcmp0( property, "id" ) ) - { - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_ID] ); - } - else if ( !g_strcmp0( property, "name" ) ) - { - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_NAME] ); - } - else if ( !g_strcmp0( property, "description" ) ) - { - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_DESCRIPTION] ); - } - else if ( !g_strcmp0( property, "volume" ) || !g_strcmp0( property, "is-muted" ) ) - { - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_VOLUME] ); - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_IS_MUTED] ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Service Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the audio service. -// -void foobar_audio_service_class_init( FoobarAudioServiceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_audio_service_get_property; - object_klass->finalize = foobar_audio_service_finalize; - - props[PROP_INPUTS] = g_param_spec_object( - "inputs", - "Inputs", - "Sorted list of input devices.", - G_TYPE_LIST_MODEL, - G_PARAM_READABLE ); - props[PROP_OUTPUTS] = g_param_spec_object( - "outputs", - "Outputs", - "Sorted list of output devices.", - G_TYPE_LIST_MODEL, - G_PARAM_READABLE ); - props[PROP_DEFAULT_INPUT] = g_param_spec_object( - "default-input", - "Default Input", - "The default input device (reference remains constant).", - FOOBAR_TYPE_AUDIO_DEVICE, - G_PARAM_READABLE ); - props[PROP_DEFAULT_OUTPUT] = g_param_spec_object( - "default-output", - "Default Output", - "The default output device (reference remains constant).", - FOOBAR_TYPE_AUDIO_DEVICE, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for the audio service. -// -void foobar_audio_service_init( FoobarAudioService* self ) -{ - self->control = gvc_mixer_control_new( "Foobar mixer control" ); - self->devices = g_list_store_new( FOOBAR_TYPE_AUDIO_DEVICE ); - - GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_audio_service_sort_func, NULL, NULL ); - self->sorted_devices = gtk_sort_list_model_new( - G_LIST_MODEL( g_object_ref( self->devices ) ), - GTK_SORTER( sorter ) ); - - GtkCustomFilter* input_filter = gtk_custom_filter_new( foobar_audio_service_input_filter_func, NULL, NULL ); - self->inputs = gtk_filter_list_model_new( - G_LIST_MODEL( g_object_ref( self->sorted_devices ) ), - GTK_FILTER( input_filter ) ); - - GtkCustomFilter* output_filter = gtk_custom_filter_new( foobar_audio_service_output_filter_func, NULL, NULL ); - self->outputs = gtk_filter_list_model_new( - G_LIST_MODEL( g_object_ref( self->sorted_devices ) ), - GTK_FILTER( output_filter ) ); - - self->default_input = foobar_audio_device_new( self, FOOBAR_AUDIO_DEVICE_INPUT ); - self->default_output = foobar_audio_device_new( self, FOOBAR_AUDIO_DEVICE_OUTPUT ); - foobar_audio_device_set_default( self->default_input, TRUE ); - foobar_audio_device_set_default( self->default_output, TRUE ); - - self->default_sink_handler_id = g_signal_connect( - self->control, - "default-sink-changed", - G_CALLBACK( foobar_audio_service_handle_default_sink_changed ), - self ); - self->default_source_handler_id = g_signal_connect( - self->control, - "default-source-changed", - G_CALLBACK( foobar_audio_service_handle_default_source_changed ), - self ); - self->stream_added_handler_id = g_signal_connect( - self->control, - "stream-added", - G_CALLBACK( foobar_audio_service_handle_stream_added ), - self ); - self->stream_removed_handler_id = g_signal_connect( - self->control, - "stream-removed", - G_CALLBACK( foobar_audio_service_handle_stream_removed ), - self ); - - gvc_mixer_control_open( self->control ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_audio_service_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarAudioService* self = (FoobarAudioService*)object; - - switch ( prop_id ) - { - case PROP_INPUTS: - g_value_set_object( value, foobar_audio_service_get_inputs( self ) ); - break; - case PROP_OUTPUTS: - g_value_set_object( value, foobar_audio_service_get_outputs( self ) ); - break; - case PROP_DEFAULT_INPUT: - g_value_set_object( value, foobar_audio_service_get_default_input( self ) ); - break; - case PROP_DEFAULT_OUTPUT: - g_value_set_object( value, foobar_audio_service_get_default_output( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for the audio service. -// -void foobar_audio_service_finalize( GObject* object ) -{ - FoobarAudioService* self = (FoobarAudioService*)object; - - g_clear_signal_handler( &self->default_sink_handler_id, self->control ); - g_clear_signal_handler( &self->default_source_handler_id, self->control ); - g_clear_signal_handler( &self->stream_added_handler_id, self->control ); - g_clear_signal_handler( &self->stream_removed_handler_id, self->control ); - - gvc_mixer_control_close( self->control ); - - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i ) - { - FoobarAudioDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i ); - device->service = NULL; - } - - self->default_input->service = NULL; - self->default_output->service = NULL; - - g_clear_object( &self->control ); - g_clear_object( &self->inputs ); - g_clear_object( &self->outputs ); - g_clear_object( &self->sorted_devices ); - g_clear_object( &self->devices ); - g_clear_object( &self->default_input ); - g_clear_object( &self->default_output ); - - G_OBJECT_CLASS( foobar_audio_service_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new audio service instance. -// -FoobarAudioService* foobar_audio_service_new( void ) -{ - return g_object_new( FOOBAR_TYPE_AUDIO_SERVICE, NULL ); -} - -// -// Get the default input device. This reference remains constant. -// -FoobarAudioDevice* foobar_audio_service_get_default_input( FoobarAudioService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL ); - return self->default_input; -} - -// -// Get the default output device. This reference remains constant. -// -FoobarAudioDevice* foobar_audio_service_get_default_output( FoobarAudioService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL ); - return self->default_output; -} - -// -// Get a sorted list of input devices. -// -GListModel* foobar_audio_service_get_inputs( FoobarAudioService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL ); - return G_LIST_MODEL( self->inputs ); -} - -// -// Get a sorted list of output devices. -// -GListModel* foobar_audio_service_get_outputs( FoobarAudioService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_AUDIO_SERVICE( self ), NULL ); - return G_LIST_MODEL( self->outputs ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called by the mixer control when the default output device has changed. -// -// This will update the "default" state of the device and set the backing stream for the static default device. -// -void foobar_audio_service_handle_default_sink_changed( - GvcMixerControl* control, - guint id, - gpointer userdata ) -{ - (void)control; - FoobarAudioService* self = (FoobarAudioService*)userdata; - - foobar_audio_service_update_default( G_LIST_MODEL( self->outputs ), id ); - - GvcMixerStream* stream = gvc_mixer_control_lookup_stream_id( self->control, id ); - foobar_audio_device_set_stream( self->default_output, stream ); -} - -// -// Called by the mixer control when the default input device has changed. -// -// This will update the "default" state of the device and set the backing stream for the static default device. -// -void foobar_audio_service_handle_default_source_changed( - GvcMixerControl* control, - guint id, - gpointer userdata ) -{ - (void)control; - FoobarAudioService* self = (FoobarAudioService*)userdata; - - foobar_audio_service_update_default( G_LIST_MODEL( self->inputs ), id ); - - GvcMixerStream* stream = gvc_mixer_control_lookup_stream_id( self->control, id ); - foobar_audio_device_set_stream( self->default_input, stream ); -} - -// -// Called when an audio device was added. -// -void foobar_audio_service_handle_stream_added( - GvcMixerControl* control, - guint id, - gpointer userdata ) -{ - (void)control; - FoobarAudioService* self = (FoobarAudioService*)userdata; - - GvcMixerStream* stream = gvc_mixer_control_lookup_stream_id( self->control, id ); - FoobarAudioDeviceKind kind; - if ( foobar_audio_service_identify_stream( stream, &kind ) ) - { - g_autoptr( FoobarAudioDevice ) device = foobar_audio_device_new( self, kind ); - foobar_audio_device_set_stream( device, stream ); - g_list_store_append( self->devices, device ); - } -} - -// -// Called when an audio device was removed. -// -void foobar_audio_service_handle_stream_removed( - GvcMixerControl* control, - guint id, - gpointer userdata ) -{ - (void)control; - FoobarAudioService* self = (FoobarAudioService*)userdata; - - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i ) - { - FoobarAudioDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i ); - if ( foobar_audio_device_get_id( device ) == id ) - { - g_object_ref( device ); - device->service = NULL; - g_list_store_remove( self->devices, i ); - foobar_audio_device_set_stream( device, NULL ); - g_object_unref( device ); - break; - } - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Methods -// --------------------------------------------------------------------------------------------------------------------- - -// -// Identify whether a stream is an input device or an output device, returning FALSE if unknown. -// -gboolean foobar_audio_service_identify_stream( - GvcMixerStream* stream, - FoobarAudioDeviceKind* out_kind ) -{ - if ( GVC_IS_MIXER_SINK( stream ) ) - { - *out_kind = FOOBAR_AUDIO_DEVICE_OUTPUT; - return TRUE; - } - - if ( GVC_IS_MIXER_SOURCE( stream ) ) - { - *out_kind = FOOBAR_AUDIO_DEVICE_INPUT; - return TRUE; - } - - return FALSE; -} - -// -// Set the "default" state for the device with the specified ID to TRUE and all other devices to FALSE. -// -void foobar_audio_service_update_default( - GListModel* list, - guint id ) -{ - for ( guint i = 0; i < g_list_model_get_n_items( list ); ++i ) - { - FoobarAudioDevice* device = g_list_model_get_item( list, i ); - gboolean is_default = foobar_audio_device_get_id( device ) == id; - foobar_audio_device_set_default( device, is_default ); - } -} - -// -// Sorting callback for audio devices. -// -gint foobar_audio_service_sort_func( - gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ) -{ - (void)userdata; - - FoobarAudioDevice* device_a = (FoobarAudioDevice*)item_a; - FoobarAudioDevice* device_b = (FoobarAudioDevice*)item_b; - return g_strcmp0( - foobar_audio_device_get_description( device_a ), - foobar_audio_device_get_description( device_b ) ); -} - -// -// Filtering callback for the list of input devices. -// -gboolean foobar_audio_service_input_filter_func( - gpointer item, - gpointer userdata ) -{ - (void)userdata; - - FoobarAudioDevice* device = item; - return foobar_audio_device_get_kind( device ) == FOOBAR_AUDIO_DEVICE_INPUT; -} - -// -// Filtering callback for the list of output devices. -// -gboolean foobar_audio_service_output_filter_func( - gpointer item, - gpointer userdata ) -{ - (void)userdata; - - FoobarAudioDevice* device = item; - return foobar_audio_device_get_kind( device ) == FOOBAR_AUDIO_DEVICE_OUTPUT; -} diff --git a/src/services/audio-service.h b/src/services/audio-service.h deleted file mode 100644 index a833c20..0000000 --- a/src/services/audio-service.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_AUDIO_DEVICE_KIND foobar_audio_device_kind_get_type( ) -#define FOOBAR_TYPE_AUDIO_DEVICE foobar_audio_device_get_type( ) -#define FOOBAR_TYPE_AUDIO_SERVICE foobar_audio_service_get_type( ) - -typedef enum -{ - FOOBAR_AUDIO_DEVICE_INPUT = 0, - FOOBAR_AUDIO_DEVICE_OUTPUT, -} FoobarAudioDeviceKind; - -GType foobar_audio_device_kind_get_type( void ); - -G_DECLARE_FINAL_TYPE( FoobarAudioDevice, foobar_audio_device, FOOBAR, AUDIO_DEVICE, GObject ) - -FoobarAudioDeviceKind foobar_audio_device_get_kind ( FoobarAudioDevice* self ); -guint foobar_audio_device_get_id ( FoobarAudioDevice* self ); -gchar const* foobar_audio_device_get_name ( FoobarAudioDevice* self ); -gchar const* foobar_audio_device_get_description( FoobarAudioDevice* self ); -gint foobar_audio_device_get_volume ( FoobarAudioDevice* self ); -gboolean foobar_audio_device_is_muted ( FoobarAudioDevice* self ); -gboolean foobar_audio_device_is_default ( FoobarAudioDevice* self ); -gboolean foobar_audio_device_is_available ( FoobarAudioDevice* self ); -void foobar_audio_device_set_volume ( FoobarAudioDevice* self, - gint value ); -void foobar_audio_device_set_muted ( FoobarAudioDevice* self, - gboolean value ); -void foobar_audio_device_make_default ( FoobarAudioDevice* self ); - -G_DECLARE_FINAL_TYPE( FoobarAudioService, foobar_audio_service, FOOBAR, AUDIO_SERVICE, GObject ) - -FoobarAudioService* foobar_audio_service_new ( void ); -FoobarAudioDevice* foobar_audio_service_get_default_input ( FoobarAudioService* self ); -FoobarAudioDevice* foobar_audio_service_get_default_output( FoobarAudioService* self ); -GListModel* foobar_audio_service_get_inputs ( FoobarAudioService* self ); -GListModel* foobar_audio_service_get_outputs ( FoobarAudioService* self ); - -G_END_DECLS diff --git a/src/services/battery-service.c b/src/services/battery-service.c deleted file mode 100644 index 0457547..0000000 --- a/src/services/battery-service.c +++ /dev/null @@ -1,378 +0,0 @@ -#include "services/battery-service.h" -#include "dbus/upower.h" -#include - -#define DEVICE_STATE_CHARGING 1 -#define DEVICE_STATE_FULLY_CHARGED 4 - -// -// FoobarBatteryState: -// -// Structure storing the current information about the battery. -// - -struct _FoobarBatteryState -{ - gboolean is_charging; - gboolean is_charged; - gint percentage; - gint time_remaining; -}; - -G_DEFINE_BOXED_TYPE( FoobarBatteryState, foobar_battery_state, foobar_battery_state_copy, foobar_battery_state_free ) - -// -// FoobarBatteryService: -// -// Service monitoring the state of the battery. This is implemented using the UPower D-Bus API. -// - -struct _FoobarBatteryService -{ - GObject parent_instance; - FoobarBatteryState* state; - FoobarUPowerDevice* proxy; - gulong changed_handler_id; -}; - -enum -{ - PROP_STATE = 1, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_battery_service_class_init ( FoobarBatteryServiceClass* klass ); -static void foobar_battery_service_init ( FoobarBatteryService* self ); -static void foobar_battery_service_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_battery_service_finalize ( GObject* object ); -static void foobar_battery_service_handle_connect ( GObject* object, - GAsyncResult* result, - gpointer userdata ); -static void foobar_battery_service_handle_properties_changed( GDBusProxy* proxy, - GVariant* changed_properties, - GStrv invalidated_properties, - gpointer userdata ); -static void foobar_battery_service_update ( FoobarBatteryService* self ); - -G_DEFINE_FINAL_TYPE( FoobarBatteryService, foobar_battery_service, G_TYPE_OBJECT ) - -// --------------------------------------------------------------------------------------------------------------------- -// State -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new, default-initialized state structure. -// -FoobarBatteryState* foobar_battery_state_new( void ) -{ - return g_new0( FoobarBatteryState, 1 ); -} - -// -// Create a mutable copy of a state structure. -// -FoobarBatteryState* foobar_battery_state_copy( FoobarBatteryState const* self ) -{ - if ( !self ) { return NULL; } - - FoobarBatteryState* copy = g_new0( FoobarBatteryState, 1 ); - copy->is_charging = self->is_charging; - copy->is_charged = self->is_charged; - copy->percentage = self->percentage; - copy->time_remaining = self->time_remaining; - return copy; -} - -// -// Release resources associated with a state structure (NULL is allowed). -// -void foobar_battery_state_free( FoobarBatteryState* self ) -{ - g_free( self ); -} - -// -// Compare two battery state structures (NULL is allowed)/ -// -gboolean foobar_battery_state_equal( - FoobarBatteryState const* a, - FoobarBatteryState const* b ) -{ - if ( a == NULL && b == NULL ) { return TRUE; } - if ( a == NULL || b == NULL ) { return FALSE; } - - if ( a->is_charging != b->is_charging ) { return FALSE; } - if ( a->is_charged != b->is_charged ) { return FALSE; } - if ( a->percentage != b->percentage ) { return FALSE; } - if ( a->time_remaining != b->time_remaining ) { return FALSE; } - - return TRUE; -} - -// -// Indicates whether the battery is currently being charged. -// -gboolean foobar_battery_state_is_charging( FoobarBatteryState const* self ) -{ - g_return_val_if_fail( self != NULL, FALSE ); - return self->is_charging; -} - -// -// Indicates whether the battery is currently being charged. -// -void foobar_battery_state_set_charging( - FoobarBatteryState* self, - gboolean value ) -{ - g_return_if_fail( self != NULL ); - self->is_charging = !!value; -} - -// -// Indicates whether the battery is fully charged. -// -gboolean foobar_battery_state_is_charged( FoobarBatteryState const* self ) -{ - g_return_val_if_fail( self != NULL, FALSE ); - return self->is_charged; -} - -// -// Indicates whether the battery is fully charged. -// -void foobar_battery_state_set_charged( - FoobarBatteryState* self, - gboolean value ) -{ - g_return_if_fail( self != NULL ); - self->is_charged = !!value; -} - -// -// Current battery level as a percentage value. -// -gint foobar_battery_state_get_percentage( FoobarBatteryState const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->percentage; -} - -// -// Current battery level as a percentage value. -// -void foobar_battery_state_set_percentage( - FoobarBatteryState* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->percentage = CLAMP( value, 0, 100 ); -} - -// -// Estimated remaining time until fully charged or empty in seconds. -// -gint foobar_battery_state_get_time_remaining( FoobarBatteryState const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->time_remaining; -} - -// -// Estimated remaining time until fully charged or empty in seconds. -// -void foobar_battery_state_set_time_remaining( - FoobarBatteryState* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->time_remaining = MAX( value, 0 ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Service Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the battery service. -// -void foobar_battery_service_class_init( FoobarBatteryServiceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_battery_service_get_property; - object_klass->finalize = foobar_battery_service_finalize; - - props[PROP_STATE] = g_param_spec_boxed( - "state", - "State", - "Current state of the battery, if available.", - FOOBAR_TYPE_BATTERY_STATE, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for the battery service. -// -void foobar_battery_service_init( FoobarBatteryService* self ) -{ - foobar_upower_device_proxy_new_for_bus( - G_BUS_TYPE_SYSTEM, - G_DBUS_PROXY_FLAGS_NONE, - "org.freedesktop.UPower", - "/org/freedesktop/UPower/devices/DisplayDevice", - NULL, - foobar_battery_service_handle_connect, - g_object_ref( self ) ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_battery_service_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarBatteryService* self = (FoobarBatteryService*)object; - - switch ( prop_id ) - { - case PROP_STATE: - g_value_set_boxed( value, foobar_battery_service_get_state( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for the battery service. -// -void foobar_battery_service_finalize( GObject* object ) -{ - FoobarBatteryService* self = (FoobarBatteryService*)object; - - g_clear_signal_handler( &self->changed_handler_id, self->proxy ); - g_clear_object( &self->proxy ); - g_clear_pointer( &self->state, foobar_battery_state_free ); - - G_OBJECT_CLASS( foobar_battery_service_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new battery service instance. -// -FoobarBatteryService* foobar_battery_service_new( void ) -{ - return g_object_new( FOOBAR_TYPE_BATTERY_SERVICE, NULL ); -} - -// -// Get the current state of the battery, if available. -// -FoobarBatteryState const* foobar_battery_service_get_state( FoobarBatteryService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_BATTERY_SERVICE( self ), NULL ); - - return self->state; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called after asynchronous initialization of the D-Bus proxy. -// -void foobar_battery_service_handle_connect( - GObject* object, - GAsyncResult* result, - gpointer userdata ) -{ - (void)object; - g_autoptr( FoobarBatteryService ) self = (FoobarBatteryService*)userdata; - - g_autoptr( GError ) error = NULL; - self->proxy = foobar_upower_device_proxy_new_for_bus_finish( result, &error ); - if ( self->proxy ) - { - self->changed_handler_id = g_signal_connect( - self->proxy, - "g-properties-changed", - G_CALLBACK( foobar_battery_service_handle_properties_changed ), - self ); - foobar_battery_service_update( self ); - } - else - { - g_warning( "Unable to connect to battery status service: %s", error->message ); - } -} - -// -// Called by the D-Bus server when the battery state has changed. -// -void foobar_battery_service_handle_properties_changed( - GDBusProxy* proxy, - GVariant* changed_properties, - GStrv invalidated_properties, - gpointer userdata ) -{ - (void)proxy; - (void)changed_properties; - (void)invalidated_properties; - FoobarBatteryService* self = (FoobarBatteryService*)userdata; - - foobar_battery_service_update( self ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Methods -// --------------------------------------------------------------------------------------------------------------------- - -// -// Reload the battery state. -// -void foobar_battery_service_update( FoobarBatteryService* self ) -{ - g_return_if_fail( FOOBAR_IS_BATTERY_SERVICE( self ) ); - - FoobarBatteryState* new_state = NULL; - if ( self->proxy && foobar_upower_device_get_is_present( self->proxy ) ) - { - guint state = foobar_upower_device_get_state( self->proxy ); - gint percent = (gint)round( foobar_upower_device_get_percentage( self->proxy ) ); - gboolean is_charging = state == DEVICE_STATE_CHARGING; - gboolean is_charged = - state == DEVICE_STATE_FULLY_CHARGED || - ( state == DEVICE_STATE_CHARGING && percent == 100 ); - gint time_remaining = is_charging - ? (gint)foobar_upower_device_get_time_to_full( self->proxy ) - : (gint)foobar_upower_device_get_time_to_empty( self->proxy ); - - new_state = foobar_battery_state_new( ); - foobar_battery_state_set_charging( new_state, is_charging ); - foobar_battery_state_set_charged( new_state, is_charged ); - foobar_battery_state_set_percentage( new_state, percent ); - foobar_battery_state_set_time_remaining( new_state, time_remaining ); - } - - if ( !foobar_battery_state_equal( self->state, new_state ) ) - { - g_clear_pointer( &self->state, foobar_battery_state_free ); - self->state = new_state; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_STATE] ); - } -} diff --git a/src/services/battery-service.h b/src/services/battery-service.h deleted file mode 100644 index bf15c4a..0000000 --- a/src/services/battery-service.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_BATTERY_STATE foobar_battery_state_get_type( ) -#define FOOBAR_TYPE_BATTERY_SERVICE foobar_battery_service_get_type( ) - -typedef struct _FoobarBatteryState FoobarBatteryState; - -GType foobar_battery_state_get_type ( void ); -FoobarBatteryState* foobar_battery_state_new ( void ); -FoobarBatteryState* foobar_battery_state_copy ( FoobarBatteryState const* self ); -void foobar_battery_state_free ( FoobarBatteryState* self ); -gboolean foobar_battery_state_equal ( FoobarBatteryState const* a, - FoobarBatteryState const* b ); -gboolean foobar_battery_state_is_charging ( FoobarBatteryState const* self ); -gboolean foobar_battery_state_is_charged ( FoobarBatteryState const* self ); -gint foobar_battery_state_get_percentage ( FoobarBatteryState const* self ); -gint foobar_battery_state_get_time_remaining( FoobarBatteryState const* self ); -void foobar_battery_state_set_charging ( FoobarBatteryState* self, - gboolean value ); -void foobar_battery_state_set_charged ( FoobarBatteryState* self, - gboolean value ); -void foobar_battery_state_set_percentage ( FoobarBatteryState* self, - gint value ); -void foobar_battery_state_set_time_remaining( FoobarBatteryState* self, - gint value ); - -G_DECLARE_FINAL_TYPE( FoobarBatteryService, foobar_battery_service, FOOBAR, BATTERY_SERVICE, GObject ) - -FoobarBatteryService* foobar_battery_service_new ( void ); -FoobarBatteryState const* foobar_battery_service_get_state( FoobarBatteryService* self ); - -G_END_DECLS diff --git a/src/services/bluetooth-service.c b/src/services/bluetooth-service.c deleted file mode 100644 index eb13968..0000000 --- a/src/services/bluetooth-service.c +++ /dev/null @@ -1,949 +0,0 @@ -#include "services/bluetooth-service.h" -#include "dbus/bluez.h" -#include - -// -// FoobarBluetoothDeviceState: -// -// The current connection state of a remote bluetooth device. -// - -G_DEFINE_ENUM_TYPE( - FoobarBluetoothDeviceState, - foobar_bluetooth_device_state, - G_DEFINE_ENUM_VALUE( FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED, "disconnected" ), - G_DEFINE_ENUM_VALUE( FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING, "connecting" ), - G_DEFINE_ENUM_VALUE( FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED, "connected" ) ) - -// -// FoobarBluetoothDevice: -// -// Represents an external bluetooth device the client can connect to. -// - -struct _FoobarBluetoothDevice -{ - GObject parent_instance; - FoobarBluetoothService* service; - FoobarBluetoothDeviceState state; - FoobarBluezDevice* device; - GCancellable* connect_cancellable; - gulong notify_handler_id; -}; - -enum -{ - DEVICE_PROP_NAME = 1, - DEVICE_PROP_STATE, - N_DEVICE_PROPS, -}; - -static GParamSpec* device_props[N_DEVICE_PROPS] = { 0 }; - -static void foobar_bluetooth_device_class_init ( FoobarBluetoothDeviceClass* klass ); -static void foobar_bluetooth_device_init ( FoobarBluetoothDevice* self ); -static void foobar_bluetooth_device_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_bluetooth_device_finalize ( GObject* object ); -static FoobarBluetoothDevice* foobar_bluetooth_device_new ( FoobarBluetoothService* service, - FoobarBluezDevice* device ); -static void foobar_bluetooth_device_handle_notify( GObject* object, - GParamSpec* pspec, - gpointer userdata ); -static void foobar_bluetooth_device_connect_cb ( GObject* object, - GAsyncResult* result, - gpointer userdata ); -static void foobar_bluetooth_device_disconnect_cb( GObject* object, - GAsyncResult* result, - gpointer userdata ); -static void foobar_bluetooth_device_update_state ( FoobarBluetoothDevice* self ); - -G_DEFINE_FINAL_TYPE( FoobarBluetoothDevice, foobar_bluetooth_device, G_TYPE_OBJECT ) - -// -// FoobarBluetoothService: -// -// Service managing the state of the bluetooth adapter. This is implemented using the Bluez DBus API. -// - -struct _FoobarBluetoothService -{ - GObject parent_instance; - GDBusObjectManager* object_manager; - GListStore* devices; - GtkFilterListModel* filtered_devices; - GtkSortListModel* sorted_devices; - GtkFilterListModel* connected_devices; - GPtrArray* adapters; // First adapter is the default one. - FoobarBluezAdapter* default_adapter; - gulong interface_added_handler_id; - gulong interface_removed_handler_id; - gulong object_added_handler_id; - gulong object_removed_handler_id; - gulong adapter_notify_handler_id; -}; - -enum -{ - PROP_DEVICES = 1, - PROP_CONNECTED_DEVICES, - PROP_IS_AVAILABLE, - PROP_IS_ENABLED, - PROP_IS_SCANNING, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_bluetooth_service_class_init ( FoobarBluetoothServiceClass* klass ); -static void foobar_bluetooth_service_init ( FoobarBluetoothService* self ); -static void foobar_bluetooth_service_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_bluetooth_service_set_property ( GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ); -static void foobar_bluetooth_service_finalize ( GObject* object ); -static void foobar_bluetooth_service_handle_interface_added ( GDBusObjectManager* manager, - GDBusObject* object, - GDBusInterface* interface, - gpointer userdata ); -static void foobar_bluetooth_service_handle_interface_removed( GDBusObjectManager* manager, - GDBusObject* object, - GDBusInterface* interface, - gpointer userdata ); -static void foobar_bluetooth_service_handle_object_added ( GDBusObjectManager* manager, - GDBusObject* object, - gpointer userdata ); -static void foobar_bluetooth_service_handle_object_removed ( GDBusObjectManager* manager, - GDBusObject* object, - gpointer userdata ); -static void foobar_bluetooth_service_handle_adapter_notify ( GObject* object, - GParamSpec* pspec, - gpointer userdata ); -static void foobar_bluetooth_service_update_default_adapter ( FoobarBluetoothService* self ); -static GType foobar_bluetooth_service_dbus_proxy_type_cb ( GDBusObjectManagerClient* manager, - gchar const* object_path, - gchar const* interface_name, - gpointer userdata ); -static gboolean foobar_bluetooth_service_filter_func ( gpointer item, - gpointer userdata ); -static gboolean foobar_bluetooth_service_connected_filter_func ( gpointer item, - gpointer userdata ); -static gint foobar_bluetooth_service_sort_func ( gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarBluetoothService, foobar_bluetooth_service, G_TYPE_OBJECT ) - -// --------------------------------------------------------------------------------------------------------------------- -// Device -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for devices. -// -void foobar_bluetooth_device_class_init( FoobarBluetoothDeviceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_bluetooth_device_get_property; - object_klass->finalize = foobar_bluetooth_device_finalize; - - device_props[DEVICE_PROP_NAME] = g_param_spec_string( - "name", - "Name", - "Human-readable name for the device.", - NULL, - G_PARAM_READABLE ); - device_props[DEVICE_PROP_STATE] = g_param_spec_enum( - "state", - "State", - "Current connection state of the device.", - FOOBAR_TYPE_BLUETOOTH_DEVICE_STATE, - 0, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_DEVICE_PROPS, device_props ); -} - -// -// Instance initialization for devices. -// -void foobar_bluetooth_device_init( FoobarBluetoothDevice* self ) -{ - (void)self; -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_bluetooth_device_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarBluetoothDevice* self = (FoobarBluetoothDevice*)object; - - switch ( prop_id ) - { - case DEVICE_PROP_NAME: - g_value_set_string( value, foobar_bluetooth_device_get_name( self ) ); - break; - case DEVICE_PROP_STATE: - g_value_set_enum( value, foobar_bluetooth_device_get_state( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for devices. -// -void foobar_bluetooth_device_finalize( GObject* object ) -{ - FoobarBluetoothDevice* self = (FoobarBluetoothDevice*)object; - - g_clear_signal_handler( &self->notify_handler_id, self->device ); - g_clear_object( &self->device ); - g_clear_object( &self->connect_cancellable ); - - G_OBJECT_CLASS( foobar_bluetooth_device_parent_class )->finalize( object ); -} - -// -// Create a new device object, wrapping a DBus device instance. -// -FoobarBluetoothDevice* foobar_bluetooth_device_new( - FoobarBluetoothService* service, - FoobarBluezDevice* device ) -{ - FoobarBluetoothDevice* self = g_object_new( FOOBAR_TYPE_BLUETOOTH_DEVICE, NULL ); - - self->service = service; - self->device = g_object_ref( device ); - foobar_bluetooth_device_update_state( self ); - - self->notify_handler_id = g_signal_connect( - self->device, "notify", G_CALLBACK( foobar_bluetooth_device_handle_notify ), self ); - - return self; -} - -// -// Get the name/alias for the device. -// -gchar const* foobar_bluetooth_device_get_name( FoobarBluetoothDevice* self ) -{ - g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_DEVICE( self ), NULL ); - return foobar_bluez_device_get_alias( self->device ); -} - -// -// Get the current connection state for the device. -// -FoobarBluetoothDeviceState foobar_bluetooth_device_get_state( FoobarBluetoothDevice* self ) -{ - g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_DEVICE( self ), 0 ); - return self->state; -} - -// -// Toggle whether the device is connected: -// -// - If the device is currently disconnected, it is connected. -// - If the device is currently being connected, that process is aborted. -// - If the device is currently connected, it is disconnected. -// -void foobar_bluetooth_device_toggle_connection( FoobarBluetoothDevice* self ) -{ - g_return_if_fail( FOOBAR_IS_BLUETOOTH_DEVICE( self ) ); - - switch ( self->state ) - { - case FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED: - self->connect_cancellable = g_cancellable_new( ); - foobar_bluetooth_device_update_state( self ); - foobar_bluez_device_call_connect( - self->device, - self->connect_cancellable, - foobar_bluetooth_device_connect_cb, - g_object_ref( self ) ); - break; - case FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING: - g_cancellable_cancel( self->connect_cancellable ); - break; - case FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED: - foobar_bluez_device_call_disconnect( - self->device, - NULL, - foobar_bluetooth_device_disconnect_cb, - g_object_ref( self ) ); - break; - default: - g_warn_if_reached( ); - } -} - -// -// Called by the underlying device when one of its properties changes. -// -void foobar_bluetooth_device_handle_notify( - GObject* object, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)object; - FoobarBluetoothDevice* self = (FoobarBluetoothDevice*)userdata; - - gchar const* property = g_param_spec_get_name( pspec ); - if ( !g_strcmp0( property, "alias" ) ) - { - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_NAME] ); - } - else if ( !g_strcmp0( property, "name" ) || !g_strcmp0( property, "adapter" ) ) - { - if ( self->service ) - { - gtk_filter_changed( - gtk_filter_list_model_get_filter( self->service->filtered_devices ), - GTK_FILTER_CHANGE_DIFFERENT ); - } - } - else if ( !g_strcmp0( property, "connected" ) ) - { - foobar_bluetooth_device_update_state( self ); - } -} - -// -// Called when a request to connect the device is completed. -// -// This is mainly used to update the device's state. -// -void foobar_bluetooth_device_connect_cb( - GObject* object, - GAsyncResult* result, - gpointer userdata ) -{ - (void)object; - g_autoptr( FoobarBluetoothDevice ) self = (FoobarBluetoothDevice*)userdata; - - g_autoptr( GError ) error = NULL; - if ( !foobar_bluez_device_call_connect_finish( self->device, result, &error ) ) - { - g_warning( "Unable to connect bluetooth device: %s", error->message ); - } - - g_clear_object( &self->connect_cancellable ); - foobar_bluetooth_device_update_state( self ); -} - -// -// Called when a request to disconnect the device is completed. -// -void foobar_bluetooth_device_disconnect_cb( - GObject* object, - GAsyncResult* result, - gpointer userdata ) -{ - (void)object; - g_autoptr( FoobarBluetoothDevice ) self = (FoobarBluetoothDevice*)userdata; - - g_autoptr( GError ) error = NULL; - if ( !foobar_bluez_device_call_disconnect_finish( self->device, result, &error ) ) - { - g_warning( "Unable to disconnect bluetooth device: %s", error->message ); - } -} - -// -// Update the current device state. -// -// The connect_cancellable member variable is used to indicate an ongoing connection attempt. Otherwise, the value of -// is_connected is used. -// -void foobar_bluetooth_device_update_state( FoobarBluetoothDevice* self ) -{ - FoobarBluetoothDeviceState new_state = foobar_bluez_device_get_connected( self->device ) - ? FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED - : FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED; - if ( self->connect_cancellable ) - { - new_state = FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING; - } - - if ( self->state != new_state ) - { - self->state = new_state; - g_object_notify_by_pspec( G_OBJECT( self ), device_props[DEVICE_PROP_STATE] ); - - if ( self->service ) - { - gtk_filter_changed( - gtk_filter_list_model_get_filter( self->service->connected_devices ), - GTK_FILTER_CHANGE_DIFFERENT ); - } - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Service Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the bluetooth service. -// -void foobar_bluetooth_service_class_init( FoobarBluetoothServiceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_bluetooth_service_get_property; - object_klass->set_property = foobar_bluetooth_service_set_property; - object_klass->finalize = foobar_bluetooth_service_finalize; - - props[PROP_DEVICES] = g_param_spec_object( - "devices", - "Devices", - "Sorted list of available bluetooth devices.", - G_TYPE_LIST_MODEL, - G_PARAM_READABLE ); - props[PROP_CONNECTED_DEVICES] = g_param_spec_object( - "connected-devices", - "Connected Devices", - "Sorted list of currently connected bluetooth devices.", - G_TYPE_LIST_MODEL, - G_PARAM_READABLE ); - props[PROP_IS_AVAILABLE] = g_param_spec_boolean( - "is-available", - "Is Available", - "Indicates whether the system has a bluetooth adapter.", - FALSE, - G_PARAM_READABLE ); - props[PROP_IS_ENABLED] = g_param_spec_boolean( - "is-enabled", - "Is Enabled", - "Indicates whether the bluetooth adapter is currently enabled.", - FALSE, - G_PARAM_READWRITE ); - props[PROP_IS_SCANNING] = g_param_spec_boolean( - "is-scanning", - "Is Scanning", - "Indicates whether the bluetooth adapter is currently scanning for devices.", - FALSE, - G_PARAM_READWRITE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for the bluetooth service. -// -void foobar_bluetooth_service_init( FoobarBluetoothService* self ) -{ - self->devices = g_list_store_new( FOOBAR_TYPE_BLUETOOTH_DEVICE ); - - GtkCustomFilter* filter = gtk_custom_filter_new( foobar_bluetooth_service_filter_func, self, NULL ); - self->filtered_devices = gtk_filter_list_model_new( - G_LIST_MODEL( g_object_ref( self->devices ) ), - GTK_FILTER( filter ) ); - - GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_bluetooth_service_sort_func, NULL, NULL ); - self->sorted_devices = gtk_sort_list_model_new( - G_LIST_MODEL( g_object_ref( self->filtered_devices ) ), - GTK_SORTER( sorter ) ); - - GtkCustomFilter* connected_filter = gtk_custom_filter_new( foobar_bluetooth_service_connected_filter_func, NULL, NULL ); - self->connected_devices = gtk_filter_list_model_new( - G_LIST_MODEL( g_object_ref( self->sorted_devices ) ), - GTK_FILTER( connected_filter ) ); - - self->adapters = g_ptr_array_new_with_free_func( g_object_unref ); - - g_autoptr( GError ) error = NULL; - self->object_manager = g_dbus_object_manager_client_new_for_bus_sync( - G_BUS_TYPE_SYSTEM, - G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START, - "org.bluez", - "/", - foobar_bluetooth_service_dbus_proxy_type_cb, - NULL, - NULL, - NULL, - &error ); - - if ( self->object_manager ) - { - self->interface_added_handler_id = g_signal_connect( - self->object_manager, - "interface-added", - G_CALLBACK( foobar_bluetooth_service_handle_interface_added ), - self ); - self->interface_removed_handler_id = g_signal_connect( - self->object_manager, - "interface-removed", - G_CALLBACK( foobar_bluetooth_service_handle_interface_removed ), - self ); - self->object_added_handler_id = g_signal_connect( - self->object_manager, - "object-added", - G_CALLBACK( foobar_bluetooth_service_handle_object_added ), - self ); - self->object_removed_handler_id = g_signal_connect( - self->object_manager, - "object-removed", - G_CALLBACK( foobar_bluetooth_service_handle_object_removed ), - self ); - - g_autolist( GDBusObject ) objects = g_dbus_object_manager_get_objects( self->object_manager ); - for ( GList* it = objects; it; it = it->next ) - { - foobar_bluetooth_service_handle_object_added( self->object_manager, it->data, self ); - } - } - else - { - g_warning( "Unable to create a DBus object manager for communication with Bluez: %s", error->message ); - } -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_bluetooth_service_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarBluetoothService* self = (FoobarBluetoothService*)object; - - switch ( prop_id ) - { - case PROP_DEVICES: - g_value_set_object( value, foobar_bluetooth_service_get_devices( self ) ); - break; - case PROP_CONNECTED_DEVICES: - g_value_set_object( value, foobar_bluetooth_service_get_connected_devices( self ) ); - break; - case PROP_IS_AVAILABLE: - g_value_set_boolean( value, foobar_bluetooth_service_is_available( self ) ); - break; - case PROP_IS_ENABLED: - g_value_set_boolean( value, foobar_bluetooth_service_is_enabled( self ) ); - break; - case PROP_IS_SCANNING: - g_value_set_boolean( value, foobar_bluetooth_service_is_scanning( 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_bluetooth_service_set_property( - GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ) -{ - FoobarBluetoothService* self = (FoobarBluetoothService*)object; - - switch ( prop_id ) - { - case PROP_IS_ENABLED: - foobar_bluetooth_service_set_enabled( self, g_value_get_boolean( value ) ); - break; - case PROP_IS_SCANNING: - foobar_bluetooth_service_set_scanning( self, g_value_get_boolean( value ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for the bluetooth service. -// -void foobar_bluetooth_service_finalize( GObject* object ) -{ - FoobarBluetoothService* self = (FoobarBluetoothService*)object; - - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i ) - { - FoobarBluetoothDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i ); - device->service = NULL; - } - - g_clear_signal_handler( &self->adapter_notify_handler_id, self->default_adapter ); - g_clear_signal_handler( &self->interface_added_handler_id, self->object_manager ); - g_clear_signal_handler( &self->interface_removed_handler_id, self->object_manager ); - g_clear_signal_handler( &self->object_added_handler_id, self->object_manager ); - g_clear_signal_handler( &self->object_removed_handler_id, self->object_manager ); - g_clear_object( &self->object_manager ); - g_clear_object( &self->connected_devices ); - g_clear_object( &self->sorted_devices ); - g_clear_object( &self->filtered_devices ); - g_clear_object( &self->devices ); - g_clear_object( &self->default_adapter ); - g_clear_pointer( &self->adapters, g_ptr_array_unref ); - - G_OBJECT_CLASS( foobar_bluetooth_service_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new bluetooth service instance. -// -FoobarBluetoothService* foobar_bluetooth_service_new( void ) -{ - return g_object_new( FOOBAR_TYPE_BLUETOOTH_SERVICE, NULL ); -} - -// -// Get the sorted list of available bluetooth devices. -// -GListModel* foobar_bluetooth_service_get_devices( FoobarBluetoothService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), NULL ); - return G_LIST_MODEL( self->sorted_devices ); -} - -// -// Get the sorted list of currently connected bluetooth devices. -// -GListModel* foobar_bluetooth_service_get_connected_devices( FoobarBluetoothService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), NULL ); - return G_LIST_MODEL( self->connected_devices ); -} - -// -// Check whether the system has a bluetooth adapter. -// -gboolean foobar_bluetooth_service_is_available( FoobarBluetoothService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), FALSE ); - return self->default_adapter != NULL; -} - -// -// Check whether the bluetooth adapter is currently enabled. -// -gboolean foobar_bluetooth_service_is_enabled( FoobarBluetoothService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), FALSE ); - return self->default_adapter ? foobar_bluez_adapter_get_powered( self->default_adapter ) : FALSE; -} - -// -// Check whether the bluetooth adapter is currently scanning for devices. -// -gboolean foobar_bluetooth_service_is_scanning( FoobarBluetoothService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ), FALSE ); - return self->default_adapter ? foobar_bluez_adapter_get_discovering( self->default_adapter ) : FALSE; -} - -// -// Update whether the bluetooth adapter is currently enabled. -// -void foobar_bluetooth_service_set_enabled( - FoobarBluetoothService* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ) ); - - if (self->default_adapter) - { - foobar_bluez_adapter_set_powered( self->default_adapter, value ); - } -} - -// -// Update whether the bluetooth adapter is currently scanning for devices. -// -void foobar_bluetooth_service_set_scanning( - FoobarBluetoothService* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_BLUETOOTH_SERVICE( self ) ); - - g_autoptr( GError ) error = NULL; - - if (self->default_adapter && foobar_bluetooth_service_is_scanning( self ) != value) - { - if (value) - { - // Make this device discoverable while discovery is active. - - GVariantBuilder filter_builder; - g_variant_builder_init( &filter_builder, G_VARIANT_TYPE_VARDICT ); - g_variant_builder_add( &filter_builder, "{sv}", "Discoverable", g_variant_new_boolean( TRUE ) ); - GVariant* filter = g_variant_builder_end( &filter_builder ); - if ( !foobar_bluez_adapter_call_set_discovery_filter_sync( self->default_adapter, filter, NULL, &error ) ) - { - g_warning( "Failed to set discovery filter: %s", error->message ); - return; - } - if ( !foobar_bluez_adapter_call_start_discovery_sync( self->default_adapter, NULL, &error ) ) - { - g_warning( "Failed to start discovery: %s", error->message ); - } - } - else - { - if ( !foobar_bluez_adapter_call_stop_discovery_sync( self->default_adapter, NULL, &error ) ) - { - g_warning( "Failed to stop discovery: %s", error->message ); - } - } - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called when an interface was added to an existing DBus object (either adapter or device). -// -void foobar_bluetooth_service_handle_interface_added( - GDBusObjectManager* manager, - GDBusObject* object, - GDBusInterface* interface, - gpointer userdata ) -{ - (void)manager; - (void)object; - FoobarBluetoothService* self = (FoobarBluetoothService*)userdata; - - if ( FOOBAR_IS_BLUEZ_DEVICE( interface ) ) - { - g_autoptr( FoobarBluetoothDevice ) device = foobar_bluetooth_device_new( - self, - FOOBAR_BLUEZ_DEVICE( interface ) ); - g_list_store_append( self->devices, device ); - } - else if ( FOOBAR_IS_BLUEZ_ADAPTER( interface ) ) - { - g_ptr_array_add( self->adapters, g_object_ref( interface ) ); - foobar_bluetooth_service_update_default_adapter( self ); - } -} - -// -// Called when an interface was removed from an existing DBus object (either adapter or device). -// -void foobar_bluetooth_service_handle_interface_removed( - GDBusObjectManager* manager, - GDBusObject* object, - GDBusInterface* interface, - gpointer userdata ) -{ - (void)manager; - FoobarBluetoothService* self = (FoobarBluetoothService*)userdata; - - gchar const* removed_path = g_dbus_object_get_object_path( object ); - if ( FOOBAR_IS_BLUEZ_DEVICE( interface ) ) - { - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->devices ) ); ++i ) - { - FoobarBluetoothDevice* device = g_list_model_get_item( G_LIST_MODEL( self->devices ), i ); - gchar const* path = g_dbus_proxy_get_object_path( G_DBUS_PROXY( device->device ) ); - if ( !g_strcmp0( path, removed_path ) ) - { - device->service = NULL; - g_list_store_remove( self->devices, i ); - break; - } - } - } - else if ( FOOBAR_IS_BLUEZ_ADAPTER( interface ) ) - { - for ( guint i = 0; i < self->adapters->len; ++i ) - { - FoobarBluezAdapter* adapter = g_ptr_array_index( self->adapters, i ); - gchar const* path = g_dbus_proxy_get_object_path( G_DBUS_PROXY( adapter ) ); - if ( !g_strcmp0( path, removed_path ) ) - { - g_ptr_array_remove_index( self->adapters, i ); - break; - } - } - foobar_bluetooth_service_update_default_adapter( self ); - } -} - -// -// Called when a DBus object was added (either adapter or device). -// -void foobar_bluetooth_service_handle_object_added( - GDBusObjectManager* manager, - GDBusObject* object, - gpointer userdata ) -{ - g_autolist( GDBusInterface ) interfaces = g_dbus_object_get_interfaces( object ); - for ( GList* it = interfaces; it; it = it->next ) - { - foobar_bluetooth_service_handle_interface_added( manager, object, it->data, userdata ); - } -} - -// -// Called when a DBus object was removed (either adapter or device). -// -void foobar_bluetooth_service_handle_object_removed( - GDBusObjectManager* manager, - GDBusObject* object, - gpointer userdata ) -{ - g_autolist( GDBusInterface ) interfaces = g_dbus_object_get_interfaces( object ); - for ( GList* it = interfaces; it; it = it->next ) - { - foobar_bluetooth_service_handle_interface_removed( manager, object, it->data, userdata ); - } -} - -// -// Called when a property of the default adapter has changed. -// -void foobar_bluetooth_service_handle_adapter_notify( - GObject* object, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)object; - FoobarBluetoothService* self = (FoobarBluetoothService*)userdata; - - gchar const* property = g_param_spec_get_name( pspec ); - if ( !g_strcmp0( property, "powered" ) ) - { - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_ENABLED] ); - } - else if ( !g_strcmp0( property, "discovering" ) ) - { - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_SCANNING] ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Methods -// --------------------------------------------------------------------------------------------------------------------- - -// -// Update the default_adapter to match the first item in the adapters array. -// -void foobar_bluetooth_service_update_default_adapter( FoobarBluetoothService* self ) -{ - FoobarBluezAdapter* new_adapter = ( self->adapters->len > 0 ) ? g_ptr_array_index( self->adapters, 0 ) : NULL; - - if ( self->default_adapter != new_adapter ) - { - g_clear_signal_handler( &self->adapter_notify_handler_id, self->default_adapter ); - g_clear_object( &self->default_adapter ); - - if ( new_adapter ) - { - self->default_adapter = g_object_ref( new_adapter ); - self->adapter_notify_handler_id = g_signal_connect( - self->default_adapter, - "notify", - G_CALLBACK( foobar_bluetooth_service_handle_adapter_notify ), - self ); - } - - gtk_filter_changed( gtk_filter_list_model_get_filter( self->filtered_devices ), GTK_FILTER_CHANGE_DIFFERENT ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_AVAILABLE] ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_ENABLED] ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_SCANNING] ); - } -} - -// -// Get the type for creating a DBus proxy object. -// -// This is used because the DBus object manager should give us instances of the FoobarBluezAdapterProxy and -// FoobarBluezDeviceProxy types. -// -GType foobar_bluetooth_service_dbus_proxy_type_cb( - GDBusObjectManagerClient* manager, - gchar const* object_path, - gchar const* interface_name, - gpointer userdata ) -{ - if ( !interface_name ) - { - return G_TYPE_DBUS_OBJECT_PROXY; - } - - if ( !g_strcmp0( interface_name, "org.bluez.Device1" ) ) - { - return FOOBAR_TYPE_BLUEZ_DEVICE_PROXY; - } - - if ( !g_strcmp0( interface_name, "org.bluez.Adapter1" ) ) - { - return FOOBAR_TYPE_BLUEZ_ADAPTER_PROXY; - } - - return G_TYPE_DBUS_PROXY; -} - -// -// Filtering callback for the list of devices, only showing devices from the default adapter. -// -gboolean foobar_bluetooth_service_filter_func( - gpointer item, - gpointer userdata ) -{ - FoobarBluetoothService* self = (FoobarBluetoothService*)userdata; - FoobarBluetoothDevice* device = item; - - gchar const* device_adapter = foobar_bluez_device_get_adapter( device->device ); - gchar const* default_adapter = self->default_adapter - ? g_dbus_proxy_get_object_path( G_DBUS_PROXY( self->default_adapter ) ) - : NULL; - gchar const* name = foobar_bluez_device_get_name( device->device ); - return name != NULL && !g_strcmp0( device_adapter, default_adapter ); -} - -// -// Filtering callback for the list of connected devices. -// -gboolean foobar_bluetooth_service_connected_filter_func( - gpointer item, - gpointer userdata ) -{ - (void)userdata; - - FoobarBluetoothDevice* device = item; - return foobar_bluetooth_device_get_state( device ) == FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED; -} - -// -// Sorting callback for the list of devices. -// -// Devices are sorted by their names. -// -gint foobar_bluetooth_service_sort_func( - gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ) -{ - (void)userdata; - - FoobarBluetoothDevice* device_a = (FoobarBluetoothDevice*)item_a; - FoobarBluetoothDevice* device_b = (FoobarBluetoothDevice*)item_b; - return g_strcmp0( foobar_bluetooth_device_get_name( device_a ), foobar_bluetooth_device_get_name( device_b ) ); -} diff --git a/src/services/bluetooth-service.h b/src/services/bluetooth-service.h deleted file mode 100644 index 684fd45..0000000 --- a/src/services/bluetooth-service.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_BLUETOOTH_DEVICE_STATE foobar_bluetooth_device_state_get_type( ) -#define FOOBAR_TYPE_BLUETOOTH_DEVICE foobar_bluetooth_device_get_type( ) -#define FOOBAR_TYPE_BLUETOOTH_ADAPTER foobar_bluetooth_adapter_get_type( ) -#define FOOBAR_TYPE_BLUETOOTH_SERVICE foobar_bluetooth_service_get_type( ) - -typedef enum -{ - FOOBAR_BLUETOOTH_DEVICE_STATE_DISCONNECTED, - FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTING, - FOOBAR_BLUETOOTH_DEVICE_STATE_CONNECTED, -} FoobarBluetoothDeviceState; - -GType foobar_bluetooth_device_state_get_type( void ); - -G_DECLARE_FINAL_TYPE( FoobarBluetoothDevice, foobar_bluetooth_device, FOOBAR, BLUETOOTH_DEVICE, GObject ) - -gchar const* foobar_bluetooth_device_get_name ( FoobarBluetoothDevice* self ); -FoobarBluetoothDeviceState foobar_bluetooth_device_get_state ( FoobarBluetoothDevice* self ); -void foobar_bluetooth_device_toggle_connection( FoobarBluetoothDevice* self ); - -G_DECLARE_FINAL_TYPE( FoobarBluetoothService, foobar_bluetooth_service, FOOBAR, BLUETOOTH_SERVICE, GObject ) - -FoobarBluetoothService* foobar_bluetooth_service_new ( void ); -GListModel* foobar_bluetooth_service_get_devices ( FoobarBluetoothService* self ); -GListModel* foobar_bluetooth_service_get_connected_devices( FoobarBluetoothService* self ); -gboolean foobar_bluetooth_service_is_available ( FoobarBluetoothService* self ); -gboolean foobar_bluetooth_service_is_enabled ( FoobarBluetoothService* self ); -gboolean foobar_bluetooth_service_is_scanning ( FoobarBluetoothService* self ); -void foobar_bluetooth_service_set_enabled ( FoobarBluetoothService* self, - gboolean value ); -void foobar_bluetooth_service_set_scanning ( FoobarBluetoothService* self, - gboolean value ); - -G_END_DECLS diff --git a/src/services/brightness-service.c b/src/services/brightness-service.c deleted file mode 100644 index 32f0501..0000000 --- a/src/services/brightness-service.c +++ /dev/null @@ -1,391 +0,0 @@ -#include "services/brightness-service.h" -#include -#include - -// -// FoobarBrightnessService: -// -// Service managing the brightness level. This is implemented by -// - for read access: monitoring the "brightness" file in a "/sys/class/backlight" subdirectory, -// - for write access: invoking the "brightnessctl" command. -// - -struct _FoobarBrightnessService -{ - GObject parent_instance; - gint percentage; - GFileMonitor* file_monitor; - gulong file_monitor_handler_id; - gchar* device_name; - gchar* file_path; - gint max_brightness; -}; - -enum -{ - PROP_PERCENTAGE = 1, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_brightness_service_class_init ( FoobarBrightnessServiceClass* klass ); -static void foobar_brightness_service_init ( FoobarBrightnessService* self ); -static void foobar_brightness_service_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_brightness_service_set_property ( GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ); -static void foobar_brightness_service_finalize ( GObject* object ); -static void foobar_brightness_service_handle_changed ( GFileMonitor* monitor, - GFile* file, - GFile* other_file, - GFileMonitorEvent event_type, - gpointer userdata ); -static gint foobar_brightness_service_load_percentage ( FoobarBrightnessService* self ); -static void foobar_brightness_service_write_percentage ( FoobarBrightnessService* self ); -static void foobar_brightness_service_write_percentage_watch_cb( GPid pid, - gint status, - gpointer userdata ); -static void foobar_brightness_service_get_info ( gchar** out_device_name, - gchar** out_file_path, - gint* out_max_brightness ); -static gint foobar_brightness_service_read_value ( gchar const* path ); -static GFileMonitor* foobar_brightness_service_monitor ( gchar const* path ); - -G_DEFINE_FINAL_TYPE( FoobarBrightnessService, foobar_brightness_service, G_TYPE_OBJECT ) - -// --------------------------------------------------------------------------------------------------------------------- -// Service Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the brightness service. -// -void foobar_brightness_service_class_init( FoobarBrightnessServiceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_brightness_service_get_property; - object_klass->set_property = foobar_brightness_service_set_property; - object_klass->finalize = foobar_brightness_service_finalize; - - props[PROP_PERCENTAGE] = g_param_spec_int( - "percentage", - "Percentage", - "Current brightness percentage.", - 0, - 100, - 100, - G_PARAM_READWRITE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for the brightness service. -// -void foobar_brightness_service_init( FoobarBrightnessService* self ) -{ - foobar_brightness_service_get_info( &self->device_name, &self->file_path, &self->max_brightness ); - self->percentage = foobar_brightness_service_load_percentage( self ); - if ( self->file_path ) - { - self->file_monitor = foobar_brightness_service_monitor( self->file_path ); - } - if ( self->file_monitor ) - { - self->file_monitor_handler_id = g_signal_connect( - self->file_monitor, - "changed", - G_CALLBACK( foobar_brightness_service_handle_changed ), - self ); - } - else - { - self->file_monitor_handler_id = 0; - } -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_brightness_service_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarBrightnessService* self = (FoobarBrightnessService*)object; - - switch ( prop_id ) - { - case PROP_PERCENTAGE: - g_value_set_int( value, foobar_brightness_service_get_percentage( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Property setter implementation, mapping a property id to a method. -// -void foobar_brightness_service_set_property( - GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ) -{ - FoobarBrightnessService* self = (FoobarBrightnessService*)object; - - switch ( prop_id ) - { - case PROP_PERCENTAGE: - foobar_brightness_service_set_percentage( self, g_value_get_int( value ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for the brightness service. -// -void foobar_brightness_service_finalize( GObject* object ) -{ - FoobarBrightnessService* self = (FoobarBrightnessService*)object; - - g_clear_signal_handler( &self->file_monitor_handler_id, self->file_monitor ); - g_clear_object( &self->file_monitor ); - g_clear_pointer( &self->device_name, g_free ); - g_clear_pointer( &self->file_path, g_free ); - - G_OBJECT_CLASS( foobar_brightness_service_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new brightness service instance. -// -FoobarBrightnessService* foobar_brightness_service_new( void ) -{ - return g_object_new( FOOBAR_TYPE_BRIGHTNESS_SERVICE, NULL ); -} - -// -// Get the current brightness percentage value. -// -gint foobar_brightness_service_get_percentage( FoobarBrightnessService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_BRIGHTNESS_SERVICE( self ), 0 ); - - return self->percentage; -} - -// -// Update the current brightness percentage value. -// -void foobar_brightness_service_set_percentage( - FoobarBrightnessService* self, - gint value ) -{ - g_return_if_fail( FOOBAR_IS_BRIGHTNESS_SERVICE( self ) ); - - value = CLAMP( value, 0, 100 ); - if ( self->percentage != value ) - { - self->percentage = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_PERCENTAGE] ); - foobar_brightness_service_write_percentage( self ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called by the file monitor when the contents of the "brightness" file have changed. -// -void foobar_brightness_service_handle_changed( - GFileMonitor* monitor, - GFile* file, - GFile* other_file, - GFileMonitorEvent event_type, - gpointer userdata ) -{ - (void)monitor; - (void)file; - (void)other_file; - (void)event_type; - FoobarBrightnessService* self = (FoobarBrightnessService*)userdata; - - gint new_percentage = foobar_brightness_service_load_percentage( self ); - if ( self->percentage != new_percentage ) - { - self->percentage = new_percentage; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_PERCENTAGE] ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Methods -// --------------------------------------------------------------------------------------------------------------------- - -// -// Load the current percentage value from the file at self->file_path. -// -// This requires file_path and max_brightness to be initialized, otherwise the brightness is assumed to be 100%. -// -gint foobar_brightness_service_load_percentage( FoobarBrightnessService* self ) -{ - if ( !self->file_path || self->max_brightness == -1 ) { return 100; } - - gint value = foobar_brightness_service_read_value( self->file_path ); - if ( value == -1 ) { return 100; } - - gdouble rel = value / (gdouble)self->max_brightness; - gint percentage = (gint)round( rel * 100. ); - return CLAMP( percentage, 0, 100 ); -} - -// -// Asynchronously update the current percentage value by invoking brightnessctl. -// -// This requires device_name to be initialized. -// -void foobar_brightness_service_write_percentage( FoobarBrightnessService* self ) -{ - if ( !self->device_name ) { return; } - - g_autofree gchar* percentage_string = g_strdup_printf( "%d%%", self->percentage ); - gchar* args[] = { "brightnessctl", "-d", self->device_name, "s", percentage_string, NULL }; - - g_autoptr( GError ) error = NULL; - GPid child_pid; - if ( !g_spawn_async( - NULL, - args, - NULL, - G_SPAWN_DO_NOT_REAP_CHILD - | G_SPAWN_SEARCH_PATH - | G_SPAWN_STDIN_FROM_DEV_NULL - | G_SPAWN_STDOUT_TO_DEV_NULL - | G_SPAWN_STDERR_TO_DEV_NULL, - NULL, - NULL, - &child_pid, - &error ) ) - { - g_warning( "Unable to spawn brightnessctl: %s", error->message ); - return; - } - - g_child_watch_add( child_pid, foobar_brightness_service_write_percentage_watch_cb, NULL ); -} - -// -// Called when the brightnessctl child process has finished. -// -void foobar_brightness_service_write_percentage_watch_cb( - GPid pid, - gint status, - gpointer userdata ) -{ - (void)userdata; - - g_autoptr( GError ) error = NULL; - if ( !g_spawn_check_wait_status( status, &error ) ) - { - g_warning( "Unable to set brightness with brightnessctl: %s", error->message ); - } - - g_spawn_close_pid( pid ); -} - -// -// Initialize information for the brightness device. -// -// This looks for the first directory in the /sys/class/backlight directories and uses it as the device name. It also -// populates out_file_path to the path of this directory and out_max_brightness to the contents of the corresponding -// max_brightness file. -// -void foobar_brightness_service_get_info( - gchar** out_device_name, - gchar** out_file_path, - gint* out_max_brightness ) -{ - *out_device_name = NULL; - *out_file_path = NULL; - *out_max_brightness = -1; - - g_autoptr( GFile ) dir = g_file_new_for_path( "/sys/class/backlight" ); - if ( !dir ) { return; } - - g_autoptr( GFileEnumerator ) enumerator = g_file_enumerate_children( - dir, - "standard::", - G_FILE_QUERY_INFO_NONE, - NULL, - NULL ); - if ( !enumerator ) { return; } - - g_autoptr( GFileInfo ) info = g_file_enumerator_next_file( enumerator, NULL, NULL ); - g_file_enumerator_close( enumerator, NULL, NULL ); - if ( !info ) { return; } - - gchar* device_name = g_strdup( g_file_info_get_name( info ) ); - gchar* current_file_path = g_strdup_printf( "/sys/class/backlight/%s/brightness", device_name ); - gchar* max_file_path = g_strdup_printf( "/sys/class/backlight/%s/max_brightness", device_name ); - *out_device_name = device_name; - *out_file_path = current_file_path; - *out_max_brightness = foobar_brightness_service_read_value( max_file_path ); - g_free( max_file_path ); -} - -// -// Read the current brightness integer (not percentage) from the given file path. -// -gint foobar_brightness_service_read_value( gchar const* path ) -{ - g_autofree gchar* contents = NULL; - g_autoptr( GError ) error = NULL; - if ( !g_file_get_contents( path, &contents, NULL, &error ) ) - { - g_warning( "Unable to read %s: %s", path, error->message ); - return -1; - } - - errno = 0; - char* endptr; - long result = strtol( contents, &endptr, 10 ); - if ( errno != 0 || endptr == contents || result > INT_MAX || result < 0 ) - { - g_warning( "Expected integer in %s, but got: %s", path, contents ); - return -1; - } - - return (gint)result; -} - -// -// Set up a file monitor for the given path. -// -GFileMonitor* foobar_brightness_service_monitor( gchar const* path ) -{ - g_autoptr( GFile ) file = g_file_new_for_path( path ); - if ( !file ) { return NULL; } - - g_autoptr( GError ) error = NULL; - GFileMonitor* monitor = g_file_monitor_file( file, G_FILE_MONITOR_NONE, NULL, &error ); - if ( !monitor ) { g_warning( "Unable to monitor brightness: %s", error->message ); } - - return monitor; -} diff --git a/src/services/brightness-service.h b/src/services/brightness-service.h deleted file mode 100644 index 2b3799e..0000000 --- a/src/services/brightness-service.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_BRIGHTNESS_SERVICE foobar_brightness_service_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarBrightnessService, foobar_brightness_service, FOOBAR, BRIGHTNESS_SERVICE, GObject ) - -FoobarBrightnessService* foobar_brightness_service_new ( void ); -gint foobar_brightness_service_get_percentage( FoobarBrightnessService* self ); -void foobar_brightness_service_set_percentage( FoobarBrightnessService* self, - gint percentage ); - -G_END_DECLS diff --git a/src/services/clock-service.c b/src/services/clock-service.c deleted file mode 100644 index c957011..0000000 --- a/src/services/clock-service.c +++ /dev/null @@ -1,141 +0,0 @@ -#include "services/clock-service.h" - -// -// FoobarClockService: -// -// Service providing an auto-updating timestamp value which can be used to display the current time. -// - -struct _FoobarClockService -{ - GObject parent_instance; - GDateTime* time; - guint source_id; -}; - -enum -{ - PROP_TIME = 1, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_clock_service_class_init ( FoobarClockServiceClass* klass ); -static void foobar_clock_service_init ( FoobarClockService* self ); -static void foobar_clock_service_get_property( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_clock_service_finalize ( GObject* object ); -static gboolean foobar_clock_service_handle_tick ( gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarClockService, foobar_clock_service, G_TYPE_OBJECT ) - -// --------------------------------------------------------------------------------------------------------------------- -// Service Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the clock service. -// -void foobar_clock_service_class_init( FoobarClockServiceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_clock_service_get_property; - object_klass->finalize = foobar_clock_service_finalize; - - props[PROP_TIME] = g_param_spec_boxed( - "time", - "Time", - "The current time, regularly updated.", - G_TYPE_DATE_TIME, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for the clock service. -// -void foobar_clock_service_init( FoobarClockService* self ) -{ - self->time = g_date_time_new_now_local( ); - self->source_id = g_timeout_add_seconds( 1, foobar_clock_service_handle_tick, self ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_clock_service_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarClockService* self = (FoobarClockService*)object; - - switch ( prop_id ) - { - case PROP_TIME: - g_value_set_boxed( value, foobar_clock_service_get_time( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for the clock service. -// -void foobar_clock_service_finalize( GObject* object ) -{ - FoobarClockService* self = (FoobarClockService*)object; - - g_clear_pointer( &self->time, g_date_time_unref ); - g_clear_handle_id( &self->source_id, g_source_remove ); - - G_OBJECT_CLASS( foobar_clock_service_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new clock service instance. -// -FoobarClockService* foobar_clock_service_new( void ) -{ - return g_object_new( FOOBAR_TYPE_CLOCK_SERVICE, NULL ); -} - -// -// Get the current snapshot of the timestamp. -// -// This may be a bit older than the value returned by g_date_time_new_now_local. -// -GDateTime* foobar_clock_service_get_time( FoobarClockService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CLOCK_SERVICE( self ), NULL ); - - return self->time; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called every time the update delay has elapsed to update the current timestamp value. -// -gboolean foobar_clock_service_handle_tick( gpointer userdata ) -{ - FoobarClockService* self = (FoobarClockService*)userdata; - - g_clear_pointer( &self->time, g_date_time_unref ); - self->time = g_date_time_new_now_local( ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_TIME] ); - - return G_SOURCE_CONTINUE; -} diff --git a/src/services/clock-service.h b/src/services/clock-service.h deleted file mode 100644 index d435d92..0000000 --- a/src/services/clock-service.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_CLOCK_SERVICE foobar_clock_service_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarClockService, foobar_clock_service, FOOBAR, CLOCK_SERVICE, GObject ) - -FoobarClockService* foobar_clock_service_new ( void ); -GDateTime* foobar_clock_service_get_time( FoobarClockService* self ); - -G_END_DECLS diff --git a/src/services/configuration-service.c b/src/services/configuration-service.c deleted file mode 100644 index f3ca010..0000000 --- a/src/services/configuration-service.c +++ /dev/null @@ -1,3248 +0,0 @@ -#include "services/configuration-service.h" -#include - -#define UPDATE_APPLY_DELAY 250 -#define CONFIGURATION_LOG_DOMAIN "foobar.conf" -#define CONFIGURATION_WARNING( ... ) g_log( CONFIGURATION_LOG_DOMAIN, G_LOG_LEVEL_WARNING, __VA_ARGS__ ) - -// -// FoobarScreenEdge: -// -// An edge of the screen. -// - -G_STATIC_ASSERT( sizeof( FoobarScreenEdge ) == sizeof( gint ) ); -G_DEFINE_ENUM_TYPE( - FoobarScreenEdge, - foobar_screen_edge, - G_DEFINE_ENUM_VALUE( FOOBAR_SCREEN_EDGE_LEFT, "left" ), - G_DEFINE_ENUM_VALUE( FOOBAR_SCREEN_EDGE_RIGHT, "right" ), - G_DEFINE_ENUM_VALUE( FOOBAR_SCREEN_EDGE_TOP, "top" ), - G_DEFINE_ENUM_VALUE( FOOBAR_SCREEN_EDGE_BOTTOM, "bottom" ) ) - -// -// FoobarOrientation: -// -// An orientation value. -// - -G_STATIC_ASSERT( sizeof( FoobarOrientation ) == sizeof( gint ) ); -G_DEFINE_ENUM_TYPE( - FoobarOrientation, - foobar_orientation, - G_DEFINE_ENUM_VALUE( FOOBAR_ORIENTATION_HORIZONTAL, "horizontal" ), - G_DEFINE_ENUM_VALUE( FOOBAR_ORIENTATION_VERTICAL, "vertical" ) ) - -// -// FoobarStatusItem: -// -// An item to display in a status panel item. -// - -G_STATIC_ASSERT( sizeof( FoobarStatusItem ) == sizeof( gint ) ); -G_DEFINE_ENUM_TYPE( - FoobarStatusItem, - foobar_status_item, - G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_NETWORK, "network" ), - G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_BLUETOOTH, "bluetooth" ), - G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_BATTERY, "battery" ), - G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_BRIGHTNESS, "brightness" ), - G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_AUDIO, "audio" ), - G_DEFINE_ENUM_VALUE( FOOBAR_STATUS_ITEM_NOTIFICATIONS, "notifications" ) ) - -// -// FoobarPanelItemKind: -// -// Enum representing the various panel item types. -// - -G_STATIC_ASSERT( sizeof( FoobarPanelItemKind ) == sizeof( gint ) ); -G_DEFINE_ENUM_TYPE( - FoobarPanelItemKind, - foobar_panel_item_kind, - G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_KIND_ICON, "icon" ), - G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_KIND_CLOCK, "clock" ), - G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_KIND_WORKSPACES, "workspaces" ), - G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_KIND_STATUS, "status" ) ) - -// -// FoobarPanelItemAction: -// -// The action to be executed when a panel item is clicked (if supported by the item). -// - -G_STATIC_ASSERT( sizeof( FoobarPanelItemAction ) == sizeof( gint ) ); -G_DEFINE_ENUM_TYPE( - FoobarPanelItemAction, - foobar_panel_item_action, - G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_ACTION_NONE, "none" ), - G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_ACTION_LAUNCHER, "launcher" ), - G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_ACTION_CONTROL_CENTER, "control-center" ) ) - -// -// FoobarPanelItemPosition: -// -// Position of a panel item in the panel. -// - -G_STATIC_ASSERT( sizeof( FoobarPanelItemPosition ) == sizeof( gint ) ); -G_DEFINE_ENUM_TYPE( - FoobarPanelItemPosition, - foobar_panel_item_position, - G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_POSITION_START, "start" ), - G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_POSITION_CENTER, "center" ), - G_DEFINE_ENUM_VALUE( FOOBAR_PANEL_ITEM_POSITION_END, "end" ) ) - -// -// FoobarControlCenterRow: -// -// A row in the "controls" section of the control center. -// - -G_STATIC_ASSERT( sizeof( FoobarControlCenterRow ) == sizeof( gint ) ); -G_DEFINE_ENUM_TYPE( - FoobarControlCenterRow, - foobar_control_center_row, - G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ROW_CONNECTIVITY, "connectivity" ), - G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ROW_AUDIO_OUTPUT, "audio-output" ), - G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ROW_AUDIO_INPUT, "audio-input" ), - G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ROW_BRIGHTNESS, "brightness" ) ) - -// -// FoobarControlCenterAlignment: -// -// Alignment of the control center along its screen edge. -// - -G_STATIC_ASSERT( sizeof( FoobarControlCenterAlignment ) == sizeof( gint ) ); -G_DEFINE_ENUM_TYPE( - FoobarControlCenterAlignment, - foobar_control_center_alignment, - G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ALIGNMENT_START, "start" ), - G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ALIGNMENT_CENTER, "center" ), - G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ALIGNMENT_END, "end" ), - G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL, "fill" ) ) - -// -// FoobarGeneralConfiguration: -// -// General application settings not specific to a single component. -// - -struct _FoobarGeneralConfiguration -{ - gchar* stylesheet; -}; - -static void foobar_general_configuration_load ( FoobarGeneralConfiguration* self, - GKeyFile* file ); -static void foobar_general_configuration_store( FoobarGeneralConfiguration const* self, - GKeyFile* file ); - -G_DEFINE_BOXED_TYPE( - FoobarGeneralConfiguration, - foobar_general_configuration, - foobar_general_configuration_copy, - foobar_general_configuration_free ) - -// -// FoobarPanelItemConfiguration: -// -// Configuration for an item in the panel. Structures of this type have a specific item kind with more properties. -// - -struct _FoobarPanelItemConfiguration -{ - FoobarPanelItemKind kind; - gchar* name; - FoobarPanelItemPosition position; - FoobarPanelItemAction action; - union - { - struct - { - gchar* icon_name; - } icon; - - struct - { - gchar* format; - } clock; - - struct - { - gint button_size; - gint spacing; - } workspaces; - - struct - { - FoobarStatusItem* items; - gsize items_count; - gint spacing; - gboolean show_labels; - gboolean enable_scrolling; - } status; - }; -}; - -static FoobarPanelItemConfiguration* foobar_panel_item_configuration_load ( GKeyFile* file, - gchar const* group ); -static void foobar_panel_item_configuration_store( FoobarPanelItemConfiguration const* self, - GKeyFile* file ); - -G_DEFINE_BOXED_TYPE( - FoobarPanelItemConfiguration, - foobar_panel_item_configuration, - foobar_panel_item_configuration_copy, - foobar_panel_item_configuration_free ) - -// -// FoobarPanelConfiguration: -// -// Configuration for the panel shown at the edge of a screen. -// - -struct _FoobarPanelConfiguration -{ - FoobarScreenEdge position; - gint margin; - gint padding; - gint size; - gint spacing; - gboolean multi_monitor; - FoobarPanelItemConfiguration** items; - gsize items_count; -}; - -static void foobar_panel_configuration_load ( FoobarPanelConfiguration* self, - GKeyFile* file ); -static void foobar_panel_configuration_store( FoobarPanelConfiguration const* self, - GKeyFile* file ); - -G_DEFINE_BOXED_TYPE( - FoobarPanelConfiguration, - foobar_panel_configuration, - foobar_panel_configuration_copy, - foobar_panel_configuration_free ) - -// -// FoobarLauncherConfiguration: -// -// Configuration for the application launcher. -// - -struct _FoobarLauncherConfiguration -{ - gint width; - gint position; - gint max_height; -}; - -static void foobar_launcher_configuration_load ( FoobarLauncherConfiguration* self, - GKeyFile* file ); -static void foobar_launcher_configuration_store( FoobarLauncherConfiguration const* self, - GKeyFile* file ); - -G_DEFINE_BOXED_TYPE( - FoobarLauncherConfiguration, - foobar_launcher_configuration, - foobar_launcher_configuration_copy, - foobar_launcher_configuration_free ) - -// -// FoobarControlCenterConfiguration: -// -// Configuration for the control center. -// - -struct _FoobarControlCenterConfiguration -{ - gint width; - gint height; - FoobarScreenEdge position; - gint offset; - gint padding; - gint spacing; - FoobarOrientation orientation; - FoobarControlCenterAlignment alignment; - FoobarControlCenterRow* rows; - gsize rows_count; -}; - -static void foobar_control_center_configuration_load ( FoobarControlCenterConfiguration* self, - GKeyFile* file ); -static void foobar_control_center_configuration_store( FoobarControlCenterConfiguration const* self, - GKeyFile* file ); - -G_DEFINE_BOXED_TYPE( - FoobarControlCenterConfiguration, - foobar_control_center_configuration, - foobar_control_center_configuration_copy, - foobar_control_center_configuration_free ) - -// -// FoobarNotificationConfiguration: -// -// Configuration for notifications and the notification area shown in the corner of the screen. -// - -struct _FoobarNotificationConfiguration -{ - gint width; - gint min_height; - gint spacing; - gint close_button_inset; - gchar* time_format; -}; - -static void foobar_notification_configuration_load ( FoobarNotificationConfiguration* self, - GKeyFile* file ); -static void foobar_notification_configuration_store( FoobarNotificationConfiguration const* self, - GKeyFile* file ); - -G_DEFINE_BOXED_TYPE( - FoobarNotificationConfiguration, - foobar_notification_configuration, - foobar_notification_configuration_copy, - foobar_notification_configuration_free ) - -// -// FoobarConfiguration: -// -// Main configuration structure containing the configuration state for all components of the application. -// - -struct _FoobarConfiguration -{ - FoobarGeneralConfiguration* general; - FoobarPanelConfiguration* panel; - FoobarLauncherConfiguration* launcher; - FoobarControlCenterConfiguration* control_center; - FoobarNotificationConfiguration* notifications; -}; - -static void foobar_configuration_load ( FoobarConfiguration* self, - GKeyFile* file ); -static void foobar_configuration_load_from_file( FoobarConfiguration* self, - gchar const* path ); -static void foobar_configuration_store ( FoobarConfiguration const* self, - GKeyFile* file ); - -G_DEFINE_BOXED_TYPE( FoobarConfiguration, foobar_configuration, foobar_configuration_copy, foobar_configuration_free ) - -// -// FoobarConfigurationService: -// -// Service monitoring the configuration state of the application specified in its config file. -// - -struct _FoobarConfigurationService -{ - GObject parent_instance; - gchar* path; - char* actual_path; - FoobarConfiguration* current; - GFileMonitor* monitor; - GFileMonitor* actual_monitor; - gulong changed_handler_id; - gulong actual_changed_handler_id; - guint update_source_id; -}; - -enum -{ - PROP_CURRENT = 1, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_configuration_service_class_init ( FoobarConfigurationServiceClass* klass ); -static void foobar_configuration_service_init ( FoobarConfigurationService* self ); -static void foobar_configuration_service_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_configuration_service_finalize ( GObject* object ); -static void foobar_configuration_service_handle_changed ( GFileMonitor* monitor, - GFile* file, - GFile* other_file, - GFileMonitorEvent event_type, - gpointer userdata ); -static void foobar_configuration_service_handle_actual_changed( GFileMonitor* monitor, - GFile* file, - GFile* other_file, - GFileMonitorEvent event_type, - gpointer userdata ); -static void foobar_configuration_service_update ( FoobarConfigurationService* self ); -static gboolean foobar_configuration_service_update_cb ( gpointer userdata ); -static GFileMonitor* foobar_configuration_service_create_monitor ( gchar const* path ); - -G_DEFINE_FINAL_TYPE( FoobarConfigurationService, foobar_configuration_service, G_TYPE_OBJECT ) - -// -// Parsing/Serialization Helpers: -// -// These functions make it easier to parse, validate and serialize various data types. -// - -typedef enum -{ - VALIDATE_NONE = 0, - VALIDATE_NON_NEGATIVE = 1 << 0, - VALIDATE_NON_ZERO = 1 << 1, - VALIDATE_POSITIVE = VALIDATE_NON_NEGATIVE | VALIDATE_NON_ZERO, - VALIDATE_FILE_URL = 1 << 2, - VALIDATE_DISTINCT = 1 << 3, -} ValidationFlags; - -static gboolean try_get_string_value ( GKeyFile* file, - gchar const* group, - gchar const* key, - ValidationFlags validation, - gchar** out ); -static gboolean try_get_string_list_value( GKeyFile* file, - gchar const* group, - gchar const* key, - ValidationFlags validation, - GStrv* out, - gsize* out_count ); -static gboolean try_get_int_value ( GKeyFile* file, - gchar const* group, - gchar const* key, - ValidationFlags validation, - gint* out ); -static gboolean try_get_enum_value ( GKeyFile* file, - gchar const* group, - gchar const* key, - GType enum_type, - ValidationFlags validation, - gint* out ); -static gboolean try_get_enum_list_value ( GKeyFile* file, - gchar const* group, - gchar const* key, - GType enum_type, - ValidationFlags validation, - gpointer* out, - gsize* out_count ); -static gboolean try_get_boolean_value ( GKeyFile* file, - gchar const* group, - gchar const* key, - ValidationFlags validation, - gboolean* out ); - -static gchar const* enum_value_to_string ( GType enum_type, - gint value ); -static gchar const** enum_list_to_string ( GType enum_type, - gconstpointer values, - gsize values_count ); -static gchar* enum_format_help_text( GType enum_type ); - -// --------------------------------------------------------------------------------------------------------------------- -// Default Configuration -// --------------------------------------------------------------------------------------------------------------------- - -static FoobarGeneralConfiguration default_general_configuration = - { - .stylesheet = "resource:///foobar/styles/default.css", - }; - -static FoobarPanelItemConfiguration default_panel_item_icon_configuration = - { - .kind = FOOBAR_PANEL_ITEM_KIND_ICON, - .name = "launcher", - .position = FOOBAR_PANEL_ITEM_POSITION_START, - .action = FOOBAR_PANEL_ITEM_ACTION_LAUNCHER, - .icon.icon_name = "fluent-grid-dots-symbolic", - }; - -static FoobarPanelItemConfiguration default_panel_item_clock_configuration = - { - .kind = FOOBAR_PANEL_ITEM_KIND_CLOCK, - .name = "clock", - .position = FOOBAR_PANEL_ITEM_POSITION_CENTER, - .action = FOOBAR_PANEL_ITEM_ACTION_NONE, - .clock.format = "%H\n%M", - }; - -static FoobarPanelItemConfiguration default_panel_item_workspaces_configuration = - { - .kind = FOOBAR_PANEL_ITEM_KIND_WORKSPACES, - .name = "workspaces", - .position = FOOBAR_PANEL_ITEM_POSITION_START, - .action = FOOBAR_PANEL_ITEM_ACTION_NONE, - .workspaces.button_size = 20, - .workspaces.spacing = 6, - }; - -static FoobarStatusItem default_panel_item_status_configuration_items[] = - { - FOOBAR_STATUS_ITEM_BATTERY, - FOOBAR_STATUS_ITEM_BRIGHTNESS, - FOOBAR_STATUS_ITEM_AUDIO, - FOOBAR_STATUS_ITEM_NETWORK, - FOOBAR_STATUS_ITEM_BLUETOOTH, - FOOBAR_STATUS_ITEM_NOTIFICATIONS - }; - -static FoobarPanelItemConfiguration default_panel_item_status_configuration = - { - .kind = FOOBAR_PANEL_ITEM_KIND_STATUS, - .name = "status", - .position = FOOBAR_PANEL_ITEM_POSITION_END, - .action = FOOBAR_PANEL_ITEM_ACTION_CONTROL_CENTER, - .status.items = default_panel_item_status_configuration_items, - .status.items_count = G_N_ELEMENTS( default_panel_item_status_configuration_items ), - .status.spacing = 6, - .status.show_labels = FALSE, - .status.enable_scrolling = FALSE, - }; - -static FoobarPanelItemConfiguration *default_panel_configuration_items[] = - { - &default_panel_item_icon_configuration, - &default_panel_item_workspaces_configuration, - &default_panel_item_clock_configuration, - &default_panel_item_status_configuration, - NULL, - }; - -static FoobarPanelConfiguration default_panel_configuration = - { - .position = FOOBAR_SCREEN_EDGE_LEFT, - .margin = 16, - .padding = 12, - .size = 48, - .spacing = 12, - .multi_monitor = TRUE, - .items = default_panel_configuration_items, - .items_count = G_N_ELEMENTS( default_panel_configuration_items ) - 1, - }; - -static FoobarLauncherConfiguration default_launcher_configuration = - { - .width = 600, - .position = 300, - .max_height = 400, - }; - -static FoobarControlCenterRow default_control_center_configuration_rows[] = - { - FOOBAR_CONTROL_CENTER_ROW_CONNECTIVITY, - FOOBAR_CONTROL_CENTER_ROW_AUDIO_OUTPUT, - FOOBAR_CONTROL_CENTER_ROW_BRIGHTNESS, - }; - -static FoobarControlCenterConfiguration default_control_center_configuration = - { - .position = FOOBAR_SCREEN_EDGE_LEFT, - .width = 400, - .height = 600, - .offset = 16, - .padding = 24, - .spacing = 12, - .orientation = FOOBAR_ORIENTATION_VERTICAL, - .alignment = FOOBAR_CONTROL_CENTER_ALIGNMENT_CENTER, - .rows = default_control_center_configuration_rows, - .rows_count = G_N_ELEMENTS( default_control_center_configuration_rows ), - }; - -static FoobarNotificationConfiguration default_notification_configuration = - { - .width = 400, - .min_height = 48, - .spacing = 16, - .close_button_inset = -6, - .time_format = "%H:%M", - }; - -static FoobarConfiguration default_configuration = - { - .general = &default_general_configuration, - .panel = &default_panel_configuration, - .launcher = &default_launcher_configuration, - .control_center = &default_control_center_configuration, - .notifications = &default_notification_configuration, - }; - -// --------------------------------------------------------------------------------------------------------------------- -// Default Configuration -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a default general configuration structure. -// -FoobarGeneralConfiguration* foobar_general_configuration_new( void ) -{ - return foobar_general_configuration_copy( &default_general_configuration ); -} - -// -// Create a mutable copy of another general configuration structure. -// -FoobarGeneralConfiguration* foobar_general_configuration_copy( FoobarGeneralConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - - FoobarGeneralConfiguration* copy = g_new0( FoobarGeneralConfiguration, 1 ); - copy->stylesheet = g_strdup( self->stylesheet ); - return copy; -} - -// -// Release resources associated with a general configuration structure. -// -void foobar_general_configuration_free( FoobarGeneralConfiguration* self ) -{ - g_return_if_fail( self != NULL ); - - g_free( self->stylesheet ); - g_free( self ); -} - -// -// Check if two general configuration structures are equal. -// -gboolean foobar_general_configuration_equal( - FoobarGeneralConfiguration const* a, - FoobarGeneralConfiguration const* b ) -{ - g_return_val_if_fail( a != NULL, FALSE ); - g_return_val_if_fail( b != NULL, FALSE ); - - if ( g_strcmp0( a->stylesheet, b->stylesheet ) ) { return FALSE; } - - return TRUE; -} - -// -// URI to the CSS stylesheet to use -- this may be a resource that's bundled with Foobar or a "file:" URI. -// -gchar const* foobar_general_configuration_get_stylesheet( FoobarGeneralConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - return self->stylesheet; -} - -// -// URI to the CSS stylesheet to use -- this may be a resource that's bundled with Foobar or a "file:" URI. -// -void foobar_general_configuration_set_stylesheet( - FoobarGeneralConfiguration* self, - gchar const* value ) -{ - g_return_if_fail( self != NULL ); - g_return_if_fail( value != NULL ); - - g_free( self->stylesheet ); - self->stylesheet = g_strdup( value ); -} - -// -// Populate a general configuration structure from the "general" section of a keyfile. -// -void foobar_general_configuration_load( - FoobarGeneralConfiguration* self, - GKeyFile* file ) -{ - g_autofree gchar* stylesheet = NULL; - if ( try_get_string_value( file, "general", "stylesheet", VALIDATE_FILE_URL, &stylesheet ) ) - { - foobar_general_configuration_set_stylesheet( self, stylesheet ); - } -} - -// -// Store a general configuration structure in the "general" section of a keyfile. -// -void foobar_general_configuration_store( - FoobarGeneralConfiguration const* self, - GKeyFile* file ) -{ - gchar const* stylesheet = foobar_general_configuration_get_stylesheet( self ); - g_key_file_set_string( file, "general", "stylesheet", stylesheet ); - g_key_file_set_comment( - file, - "general", - "stylesheet", - " URI to the CSS stylesheet to use -- this may be a resource that's bundled with Foobar or a \"file:\" URI.", - NULL ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Panel Item Configuration -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a default icon panel item configuration structure. -// -FoobarPanelItemConfiguration* foobar_panel_item_icon_configuration_new( void ) -{ - return foobar_panel_item_configuration_copy( &default_panel_item_icon_configuration ); -} - -// -// Create a default clock panel item configuration structure. -// -FoobarPanelItemConfiguration* foobar_panel_item_clock_configuration_new( void ) -{ - return foobar_panel_item_configuration_copy( &default_panel_item_clock_configuration ); -} - -// -// Create a default workspaces panel item configuration structure. -// -FoobarPanelItemConfiguration* foobar_panel_item_workspaces_configuration_new( void ) -{ - return foobar_panel_item_configuration_copy( &default_panel_item_workspaces_configuration ); -} - -// -// Create a default status panel item configuration structure. -// -FoobarPanelItemConfiguration* foobar_panel_item_status_configuration_new( void ) -{ - return foobar_panel_item_configuration_copy( &default_panel_item_status_configuration ); -} - -// -// Create a mutable copy of another panel item configuration structure, preserving its item type. -// -FoobarPanelItemConfiguration* foobar_panel_item_configuration_copy( FoobarPanelItemConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - - FoobarPanelItemConfiguration* copy = g_new0( FoobarPanelItemConfiguration, 1 ); - copy->kind = self->kind; - copy->name = g_strdup( self->name ); - copy->position = self->position; - copy->action = self->action; - switch ( copy->kind ) - { - case FOOBAR_PANEL_ITEM_KIND_ICON: - copy->icon.icon_name = g_strdup( self->icon.icon_name ); - break; - case FOOBAR_PANEL_ITEM_KIND_CLOCK: - copy->clock.format = g_strdup( self->clock.format ); - break; - case FOOBAR_PANEL_ITEM_KIND_WORKSPACES: - copy->workspaces.button_size = self->workspaces.button_size; - copy->workspaces.spacing = self->workspaces.spacing; - break; - case FOOBAR_PANEL_ITEM_KIND_STATUS: - copy->status.items = g_new( FoobarStatusItem, self->status.items_count ); - copy->status.items_count = self->status.items_count; - memcpy( copy->status.items, self->status.items, sizeof( *copy->status.items ) * copy->status.items_count ); - copy->status.spacing = self->status.spacing; - copy->status.show_labels = self->status.show_labels; - copy->status.enable_scrolling = self->status.enable_scrolling; - break; - default: - g_warn_if_reached( ); - } - return copy; -} - -// -// Release resources associated with a panel item configuration structure. -// -void foobar_panel_item_configuration_free( FoobarPanelItemConfiguration* self ) -{ - g_return_if_fail( self != NULL ); - - switch ( self->kind ) - { - case FOOBAR_PANEL_ITEM_KIND_ICON: - g_free( self->icon.icon_name ); - break; - case FOOBAR_PANEL_ITEM_KIND_CLOCK: - g_free( self->clock.format ); - break; - case FOOBAR_PANEL_ITEM_KIND_WORKSPACES: - break; - case FOOBAR_PANEL_ITEM_KIND_STATUS: - g_free( self->status.items ); - break; - default: - g_warn_if_reached( ); - break; - } - - g_free( self->name ); - g_free( self ); -} - -// -// Check if two panel item configuration structures are equal. -// -gboolean foobar_panel_item_configuration_equal( - FoobarPanelItemConfiguration const* a, - FoobarPanelItemConfiguration const* b ) -{ - g_return_val_if_fail( a != NULL, FALSE ); - g_return_val_if_fail( b != NULL, FALSE ); - - if ( a->kind != b->kind ) { return FALSE; } - if ( a->position != b->position ) { return FALSE; } - if ( a->action != b->action ) { return FALSE; } - if ( g_strcmp0( a->name, b->name ) ) { return FALSE; } - - switch ( a->kind ) - { - case FOOBAR_PANEL_ITEM_KIND_ICON: - if ( g_strcmp0( a->icon.icon_name, b->icon.icon_name ) ) { return FALSE; } - break; - case FOOBAR_PANEL_ITEM_KIND_CLOCK: - if ( g_strcmp0( a->clock.format, b->clock.format ) ) { return FALSE; } - break; - case FOOBAR_PANEL_ITEM_KIND_WORKSPACES: - if ( a->workspaces.button_size != b->workspaces.button_size ) { return FALSE; } - if ( a->workspaces.spacing != b->workspaces.spacing ) { return FALSE; } - break; - case FOOBAR_PANEL_ITEM_KIND_STATUS: - if ( a->status.items_count != b->status.items_count ) { return FALSE; } - if ( memcmp( a->status.items, b->status.items, sizeof( *a->status.items ) * a->status.items_count ) ) { return FALSE; } - if ( a->status.spacing != b->status.spacing ) { return FALSE; } - if ( a->status.show_labels != b->status.show_labels ) { return FALSE; } - if ( a->status.enable_scrolling != b->status.enable_scrolling ) { return FALSE; } - break; - default: - g_warn_if_reached( ); - break; - } - - return TRUE; -} - -// -// The type of panel item to be configured by this structure. -// -FoobarPanelItemKind foobar_panel_item_configuration_get_kind( FoobarPanelItemConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->kind; -} - -// -// Name of the panel item (this is derived from the section name, which has the form "panel.[name]"). -// -gchar const* foobar_panel_item_configuration_get_name( FoobarPanelItemConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->name; -} - -// -// Position where the item should be placed item within the panel. -// -FoobarPanelItemPosition foobar_panel_item_configuration_get_position( FoobarPanelItemConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->position; -} - -// -// Action invoked when the user clicks the item (if supported by the item). -// -FoobarPanelItemAction foobar_panel_item_configuration_get_action( FoobarPanelItemConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->action; -} - -// -// The GTK icon to use for the item. -// -gchar const* foobar_panel_item_icon_configuration_get_icon_name( FoobarPanelItemConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_ICON, NULL ); - - return self->icon.icon_name; -} - -// -// The time format string as used by g_date_time_format. -// -gchar const* foobar_panel_item_clock_configuration_get_format( FoobarPanelItemConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_CLOCK, NULL ); - - return self->clock.format; -} - -// -// Size of each workspace button. -// -gint foobar_panel_item_workspaces_configuration_get_button_size( FoobarPanelItemConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_WORKSPACES, 0 ); - - return self->workspaces.button_size; -} - -// -// Inner spacing between the workspace buttons. -// -gint foobar_panel_item_workspaces_configuration_get_spacing( FoobarPanelItemConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_WORKSPACES, 0 ); - - return self->workspaces.spacing; -} - -// -// Ordered list of status items to display in the item. -// -FoobarStatusItem const* foobar_panel_item_status_configuration_get_items( - FoobarPanelItemConfiguration const* self, - gsize* out_count ) -{ - g_return_val_if_fail( self != NULL, NULL ); - g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS, NULL ); - g_return_val_if_fail( out_count != NULL, NULL ); - - *out_count = self->status.items_count; - return self->status.items; -} - -// -// Inner spacing between the status items. -// -gint foobar_panel_item_status_configuration_get_spacing( FoobarPanelItemConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS, 0 ); - - return self->status.spacing; -} - -// -// Indicates whether text labels should be shown next to the status icons. -// -gboolean foobar_panel_item_status_configuration_get_show_labels( FoobarPanelItemConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, FALSE ); - g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS, FALSE ); - - return self->status.show_labels; -} - -// -// If set to true, some settings like volume or brightness can be adjusted by scrolling while hovering over the status -// item. -// -gboolean foobar_panel_item_status_configuration_get_enable_scrolling( FoobarPanelItemConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, FALSE ); - g_return_val_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS, FALSE ); - - return self->status.enable_scrolling; -} - -// -// Name of the panel item (this is derived from the section name, which has the form "panel.[name]"). -// -void foobar_panel_item_configuration_set_name( - FoobarPanelItemConfiguration* self, - gchar const* value ) -{ - g_return_if_fail( self != NULL ); - g_return_if_fail( value != NULL ); - - g_free( self->name ); - self->name = g_strdup( value ); -} - -// -// Position where the item should be placed item within the panel. -// -void foobar_panel_item_configuration_set_position( - FoobarPanelItemConfiguration* self, - FoobarPanelItemPosition value ) -{ - g_return_if_fail( self != NULL ); - self->position = value; -} - -// -// Action invoked when the user clicks the item (if supported by the item). -// -void foobar_panel_item_configuration_set_action( - FoobarPanelItemConfiguration* self, - FoobarPanelItemAction value ) -{ - g_return_if_fail( self != NULL ); - self->action = value; -} - -// -// The GTK icon to use for the item. -// -void foobar_panel_item_icon_configuration_set_icon_name( - FoobarPanelItemConfiguration* self, - gchar const* value ) -{ - g_return_if_fail( self != NULL ); - g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_ICON ); - g_return_if_fail( value != NULL ); - - g_free( self->icon.icon_name ); - self->icon.icon_name = g_strdup( value ); -} - -// -// The time format string as used by g_date_time_format. -// -void foobar_panel_item_clock_configuration_set_format( - FoobarPanelItemConfiguration* self, - gchar const* value ) -{ - g_return_if_fail( self != NULL ); - g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_CLOCK ); - g_return_if_fail( value != NULL ); - - g_free( self->clock.format ); - self->clock.format = g_strdup( value ); -} - -// -// Size of each workspace button. -// -void foobar_panel_item_workspaces_configuration_set_button_size( - FoobarPanelItemConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_WORKSPACES ); - - self->workspaces.button_size = value; -} - -// -// Inner spacing between the workspace buttons. -// -void foobar_panel_item_workspaces_configuration_set_spacing( - FoobarPanelItemConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_WORKSPACES ); - - self->workspaces.spacing = value; -} - -// -// Ordered list of status items to display in the item. -// -void foobar_panel_item_status_configuration_set_items( - FoobarPanelItemConfiguration* self, - FoobarStatusItem const* value, - gsize value_count ) -{ - g_return_if_fail( self != NULL ); - g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS ); - g_return_if_fail( value != NULL || value_count == 0 ); - - g_free( self->status.items ); - self->status.items = g_new( FoobarStatusItem, value_count ); - self->status.items_count = value_count; - memcpy( self->status.items, value, sizeof( *self->status.items ) * self->status.items_count ); -} - -// -// Inner spacing between the status items. -// -void foobar_panel_item_status_configuration_set_spacing( - FoobarPanelItemConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS ); - - self->status.spacing = value; -} - -// -// Indicates whether text labels should be shown next to the status icons. -// -void foobar_panel_item_status_configuration_set_show_labels( - FoobarPanelItemConfiguration* self, - gboolean value ) -{ - g_return_if_fail( self != NULL ); - g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS ); - - self->status.show_labels = value; -} - -// -// If set to true, some settings like volume or brightness can be adjusted by scrolling while hovering over the status -// item. -// -void foobar_panel_item_status_configuration_set_enable_scrolling( - FoobarPanelItemConfiguration* self, - gboolean value ) -{ - g_return_if_fail( self != NULL ); - g_return_if_fail( self->kind == FOOBAR_PANEL_ITEM_KIND_STATUS ); - - self->status.enable_scrolling = value; -} - -// -// Create a panel item configuration structure from the given section of a keyfile, returning NULL if it is not a panel -// item configuration section. -// -FoobarPanelItemConfiguration* foobar_panel_item_configuration_load( - GKeyFile* file, - gchar const* group ) -{ - if ( !g_str_has_prefix( group, "panel." ) ) - { - return NULL; - } - - gint kind; - if ( !try_get_enum_value( file, group, "kind", FOOBAR_TYPE_PANEL_ITEM_KIND, VALIDATE_NONE, &kind ) ) - { - return NULL; - } - - FoobarPanelItemConfiguration* result; - switch ( (FoobarPanelItemKind)kind ) - { - case FOOBAR_PANEL_ITEM_KIND_ICON: - { - result = foobar_panel_item_icon_configuration_new( ); - - g_autofree gchar* icon_name = NULL; - if ( try_get_string_value( file, group, "icon-name", VALIDATE_NONE, &icon_name ) ) - { - foobar_panel_item_icon_configuration_set_icon_name( result, icon_name ); - } - - break; - } - case FOOBAR_PANEL_ITEM_KIND_CLOCK: - { - result = foobar_panel_item_clock_configuration_new( ); - - g_autofree gchar* format = NULL; - if ( try_get_string_value( file, group, "format", VALIDATE_NONE, &format ) ) - { - foobar_panel_item_clock_configuration_set_format( result, format ); - } - - break; - } - case FOOBAR_PANEL_ITEM_KIND_WORKSPACES: - { - result = foobar_panel_item_workspaces_configuration_new( ); - - gint button_size; - if ( try_get_int_value( file, group, "button-size", VALIDATE_POSITIVE, &button_size ) ) - { - foobar_panel_item_workspaces_configuration_set_button_size( result, button_size ); - } - - gint spacing; - if ( try_get_int_value( file, group, "spacing", VALIDATE_NON_NEGATIVE, &spacing ) ) - { - foobar_panel_item_workspaces_configuration_set_spacing( result, spacing ); - } - - break; - } - case FOOBAR_PANEL_ITEM_KIND_STATUS: - { - result = foobar_panel_item_status_configuration_new( ); - - g_autofree gpointer items = NULL; - gsize items_count; - if ( try_get_enum_list_value( file, group, "items", FOOBAR_TYPE_STATUS_ITEM, VALIDATE_DISTINCT, &items, &items_count ) ) - { - foobar_panel_item_status_configuration_set_items( result, items, items_count ); - } - - gint spacing; - if ( try_get_int_value( file, group, "spacing", VALIDATE_NON_NEGATIVE, &spacing ) ) - { - foobar_panel_item_status_configuration_set_spacing( result, spacing ); - } - - gboolean show_labels; - if ( try_get_boolean_value( file, group, "show-labels", VALIDATE_NONE, &show_labels ) ) - { - foobar_panel_item_status_configuration_set_show_labels( result, show_labels ); - } - - gboolean enable_scrolling; - if ( try_get_boolean_value( file, group, "enable-scrolling", VALIDATE_NONE, &enable_scrolling ) ) - { - foobar_panel_item_status_configuration_set_enable_scrolling( result, enable_scrolling ); - } - - break; - } - default: - { - g_warn_if_reached( ); - return NULL; - } - } - - gchar const* name = group + strlen( "panel." ); - foobar_panel_item_configuration_set_name( result, name ); - - gint position; - if ( try_get_enum_value( file, group, "position", FOOBAR_TYPE_PANEL_ITEM_POSITION, VALIDATE_NONE, &position ) ) - { - foobar_panel_item_configuration_set_position( result, position ); - } - - gint action; - if ( try_get_enum_value( file, group, "action", FOOBAR_TYPE_PANEL_ITEM_ACTION, VALIDATE_NONE, &action ) ) - { - foobar_panel_item_configuration_set_action( result, action ); - } - - return result; -} - -// -// Store a panel item configuration structure in the "panel.[name]" section of a keyfile. -// -void foobar_panel_item_configuration_store( - FoobarPanelItemConfiguration const* self, - GKeyFile* file ) -{ - gchar const* name = foobar_panel_item_configuration_get_name( self ); - g_autofree gchar* group = g_strdup_printf( "panel.%s", name ); - - FoobarPanelItemKind kind = foobar_panel_item_configuration_get_kind( self ); - g_key_file_set_string( file, group, "kind", enum_value_to_string( FOOBAR_TYPE_PANEL_ITEM_KIND, kind ) ); - - FoobarPanelItemPosition position = foobar_panel_item_configuration_get_position( self ); - g_key_file_set_string( file, group, "position", enum_value_to_string( FOOBAR_TYPE_PANEL_ITEM_POSITION, position ) ); - - FoobarPanelItemAction action = foobar_panel_item_configuration_get_action( self ); - g_key_file_set_string( file, group, "action", enum_value_to_string( FOOBAR_TYPE_PANEL_ITEM_ACTION, action ) ); - - switch ( kind ) - { - case FOOBAR_PANEL_ITEM_KIND_ICON: - { - gchar const* icon_name = foobar_panel_item_icon_configuration_get_icon_name( self ); - g_key_file_set_string( file, group, "icon-name", icon_name ); - g_key_file_set_comment( - file, - group, - "icon-name", - " The GTK icon to use for the item.", - NULL ); - - break; - } - case FOOBAR_PANEL_ITEM_KIND_CLOCK: - { - gchar const* format = foobar_panel_item_clock_configuration_get_format( self ); - g_key_file_set_string( file, group, "format", format ); - g_key_file_set_comment( - file, - group, - "format", - " The time format string as used by g_date_time_format.", - NULL ); - - break; - } - case FOOBAR_PANEL_ITEM_KIND_WORKSPACES: - { - gint button_size = foobar_panel_item_workspaces_configuration_get_button_size( self ); - g_key_file_set_integer( file, group, "button-size", button_size ); - g_key_file_set_comment( - file, - group, - "button-size", - " Size of each workspace button.", - NULL ); - - gint spacing = foobar_panel_item_workspaces_configuration_get_spacing( self ); - g_key_file_set_integer( file, group, "spacing", spacing ); - g_key_file_set_comment( - file, - group, - "spacing", - " Inner spacing between the workspace buttons.", - NULL ); - - break; - } - case FOOBAR_PANEL_ITEM_KIND_STATUS: - { - gsize items_count; - FoobarStatusItem const* items = foobar_panel_item_status_configuration_get_items( self, &items_count ); - g_autofree gchar const** items_str = enum_list_to_string( FOOBAR_TYPE_STATUS_ITEM, items, items_count ); - g_key_file_set_string_list( file, group, "items", items_str, items_count ); - g_key_file_set_comment( - file, - group, - "items", - " Ordered list of status items to display in the item.", - NULL ); - - gint spacing = foobar_panel_item_status_configuration_get_spacing( self ); - g_key_file_set_integer( file, group, "spacing", spacing ); - g_key_file_set_comment( - file, - group, - "spacing", - " Inner spacing between the status items.", - NULL ); - - gboolean show_labels = foobar_panel_item_status_configuration_get_show_labels( self ); - g_key_file_set_boolean( file, group, "show-labels", show_labels ); - g_key_file_set_comment( - file, - group, - "show-labels", - " Indicates whether text labels should be shown next to the status icons.", - NULL ); - - gboolean enable_scrolling = foobar_panel_item_status_configuration_get_enable_scrolling( self ); - g_key_file_set_boolean( file, group, "enable-scrolling", enable_scrolling ); - g_key_file_set_comment( - file, - group, - "enable-scrolling", - " If set to true, some settings like volume or brightness can be adjusted by scrolling while hovering" - " over the status item.", - NULL ); - - break; - } - default: - { - g_warn_if_reached( ); - break; - } - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Panel Configuration -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a default panel configuration structure. -// -FoobarPanelConfiguration* foobar_panel_configuration_new( void ) -{ - return foobar_panel_configuration_copy( &default_panel_configuration ); -} - -// -// Create a mutable copy of another panel configuration structure. -// -FoobarPanelConfiguration* foobar_panel_configuration_copy( FoobarPanelConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - - FoobarPanelConfiguration* copy = g_new0( FoobarPanelConfiguration, 1 ); - copy->position = self->position; - copy->margin = self->margin; - copy->padding = self->padding; - copy->size = self->size; - copy->spacing = self->spacing; - copy->multi_monitor = self->multi_monitor; - copy->items = g_new( FoobarPanelItemConfiguration*, self->items_count + 1 ); - copy->items_count = self->items_count; - for ( gsize i = 0; i < copy->items_count; ++i ) - { - copy->items[i] = foobar_panel_item_configuration_copy( self->items[i] ); - } - return copy; -} - -// -// Release resources associated with a panel configuration structure. -// -void foobar_panel_configuration_free( FoobarPanelConfiguration* self ) -{ - g_return_if_fail( self != NULL ); - - for ( gsize i = 0; i < self->items_count; ++i ) - { - foobar_panel_item_configuration_free( self->items[i] ); - } - g_free( self->items ); - g_free( self ); -} - -// -// Check if two panel configuration structures are equal. -// -gboolean foobar_panel_configuration_equal( - FoobarPanelConfiguration const* a, - FoobarPanelConfiguration const* b ) -{ - g_return_val_if_fail( a != NULL, FALSE ); - g_return_val_if_fail( b != NULL, FALSE ); - - if ( a->position != b->position ) { return FALSE; } - if ( a->margin != b->margin ) { return FALSE; } - if ( a->padding != b->padding ) { return FALSE; } - if ( a->size != b->size ) { return FALSE; } - if ( a->spacing != b->spacing ) { return FALSE; } - if ( a->multi_monitor != b->multi_monitor ) { return FALSE; } - if ( a->items_count != b->items_count ) { return FALSE; } - - for ( gsize i = 0; i < a->items_count; ++i ) - { - if ( !foobar_panel_item_configuration_equal( a->items[i], b->items[i] ) ) { return FALSE; } - } - - return TRUE; -} - -// -// The screen edge which the panel should appear on. -// -FoobarScreenEdge foobar_panel_configuration_get_position( FoobarPanelConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->position; -} - -// -// Offset of the panel from all screen edges. -// -gint foobar_panel_configuration_get_margin( FoobarPanelConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->margin; -} - -// -// Inset of the panel along its orientation. -// -gint foobar_panel_configuration_get_padding( FoobarPanelConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->padding; -} - -// -// Size of the panel (depending on the orientation, this can be either the width or the height). -// -gint foobar_panel_configuration_get_size( FoobarPanelConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->size; -} - -// -// Spacing between panel items. -// -gint foobar_panel_configuration_get_spacing( FoobarPanelConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->spacing; -} - -// -// Flag to enable the panel on all monitors. -// -gboolean foobar_panel_configuration_get_multi_monitor( FoobarPanelConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, FALSE ); - return self->multi_monitor; -} - -// -// Items configured to be displayed in the panel. -// -FoobarPanelItemConfiguration const* const* foobar_panel_configuration_get_items( - FoobarPanelConfiguration const* self, - gsize* out_count ) -{ - g_return_val_if_fail( self != NULL, NULL ); - - if ( out_count ) { *out_count = self->items_count; } - return (FoobarPanelItemConfiguration const* const*)self->items; -} - -// -// Mutable items configured to be displayed in the panel. -// -FoobarPanelItemConfiguration* const* foobar_panel_configuration_get_items_mut( - FoobarPanelConfiguration* self, - gsize* out_count ) -{ - g_return_val_if_fail( self != NULL, NULL ); - - if ( out_count ) { *out_count = self->items_count; } - return self->items; -} - -// -// The screen edge which the panel should appear on. -// -void foobar_panel_configuration_set_position( - FoobarPanelConfiguration* self, - FoobarScreenEdge value ) -{ - g_return_if_fail( self != NULL ); - self->position = value; -} - -// -// Offset of the panel from all screen edges. -// -void foobar_panel_configuration_set_margin( - FoobarPanelConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->margin = value; -} - -// -// Inset of the panel along its orientation. -// -void foobar_panel_configuration_set_padding( - FoobarPanelConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->padding = value; -} - -// -// Size of the panel (depending on the orientation, this can be either the width or the height). -// -void foobar_panel_configuration_set_size( - FoobarPanelConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->size = value; -} - -// -// Spacing between panel items. -// -void foobar_panel_configuration_set_spacing( - FoobarPanelConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->spacing = value; -} - -// -// Flag to enable the panel on all monitors. -// -void foobar_panel_configuration_set_multi_monitor( - FoobarPanelConfiguration* self, - gboolean value ) -{ - g_return_if_fail( self != NULL ); - self->multi_monitor = value; -} - -// -// Items configured to be displayed in the panel. -// -void foobar_panel_configuration_set_items( - FoobarPanelConfiguration* self, - FoobarPanelItemConfiguration const* const* value, - gsize value_count ) -{ - g_return_if_fail( self != NULL ); - g_return_if_fail( value != NULL || value_count == 0 ); - - for ( gsize i = 0; i < self->items_count; ++i ) - { - foobar_panel_item_configuration_free( self->items[i] ); - } - g_free( self->items ); - self->items = g_new0( FoobarPanelItemConfiguration*, value_count + 1 ); - self->items_count = 0; - for ( gsize i = 0; i < value_count; ++i ) - { - g_warn_if_fail( value[i] != NULL ); - if ( value[i] ) - { - self->items[self->items_count++] = foobar_panel_item_configuration_copy( value[i] ); - } - } -} - -// -// Populate a panel configuration structure from the "panel" section of a keyfile. -// -void foobar_panel_configuration_load( - FoobarPanelConfiguration* self, - GKeyFile* file ) -{ - gint position; - if ( try_get_enum_value( file, "panel", "position", FOOBAR_TYPE_SCREEN_EDGE, VALIDATE_NONE, &position ) ) - { - foobar_panel_configuration_set_position( self, position ); - } - - gint margin; - if ( try_get_int_value( file, "panel", "margin", VALIDATE_NON_NEGATIVE, &margin ) ) - { - foobar_panel_configuration_set_margin( self, margin ); - } - - gint padding; - if ( try_get_int_value( file, "panel", "padding", VALIDATE_NON_NEGATIVE, &padding ) ) - { - foobar_panel_configuration_set_padding( self, padding ); - } - - gint size; - if ( try_get_int_value( file, "panel", "size", VALIDATE_POSITIVE, &size ) ) - { - foobar_panel_configuration_set_size( self, size ); - } - - gint spacing; - if ( try_get_int_value( file, "panel", "spacing", VALIDATE_NON_NEGATIVE, &spacing ) ) - { - foobar_panel_configuration_set_spacing( self, spacing ); - } - - gboolean multi_monitor; - if ( try_get_boolean_value( file, "panel", "multi-monitor", VALIDATE_NONE, &multi_monitor ) ) - { - foobar_panel_configuration_set_multi_monitor( self, multi_monitor ); - } - - g_autoptr( GPtrArray ) items = g_ptr_array_new_with_free_func( (GDestroyNotify)foobar_panel_item_configuration_free ); - g_auto( GStrv ) groups = g_key_file_get_groups( file, NULL ); - for ( gchar** it = groups; *it; ++it ) - { - FoobarPanelItemConfiguration* item = foobar_panel_item_configuration_load( file, *it ); - if ( item ) { g_ptr_array_add( items, item ); } - } - foobar_panel_configuration_set_items( self, (FoobarPanelItemConfiguration const* const*)items->pdata, items->len ); -} - -// -// Store a panel configuration structure in the "panel" section of a keyfile. -// -void foobar_panel_configuration_store( - FoobarPanelConfiguration const* self, - GKeyFile* file ) -{ - FoobarScreenEdge position = foobar_panel_configuration_get_position( self ); - g_key_file_set_string( file, "panel", "position", enum_value_to_string( FOOBAR_TYPE_SCREEN_EDGE, position ) ); - g_key_file_set_comment( - file, - "panel", - "position", - " The screen edge which the panel should appear on.", - NULL ); - - gint margin = foobar_panel_configuration_get_margin( self ); - g_key_file_set_integer( file, "panel", "margin", margin ); - g_key_file_set_comment( - file, - "panel", - "margin", - " Offset of the panel from all screen edges.", - NULL ); - - gint padding = foobar_panel_configuration_get_padding( self ); - g_key_file_set_integer( file, "panel", "padding", padding ); - g_key_file_set_comment( - file, - "panel", - "padding", - " Inset of the panel along its orientation.", - NULL ); - - gint size = foobar_panel_configuration_get_size( self ); - g_key_file_set_integer( file, "panel", "size", size ); - g_key_file_set_comment( - file, - "panel", - "size", - " Size of the panel (depending on the orientation, this can be either the width or the height).", - NULL ); - - gint spacing = foobar_panel_configuration_get_spacing( self ); - g_key_file_set_integer( file, "panel", "spacing", spacing ); - g_key_file_set_comment( - file, - "panel", - "spacing", - " Spacing between panel items.", - NULL ); - - gboolean multi_monitor = foobar_panel_configuration_get_multi_monitor( self ); - g_key_file_set_boolean( file, "panel", "multi-monitor", multi_monitor ); - g_key_file_set_comment( - file, - "panel", - "multi-monitor", - " Flag to enable the panel on all monitors.", - NULL ); - - gsize items_count; - FoobarPanelItemConfiguration const* const* items = foobar_panel_configuration_get_items( self, &items_count ); - for ( gsize i = 0; i < items_count; ++i ) - { - foobar_panel_item_configuration_store( items[i], file ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Launcher Configuration -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a default launcher configuration structure. -// -FoobarLauncherConfiguration* foobar_launcher_configuration_new( void ) -{ - return foobar_launcher_configuration_copy( &default_launcher_configuration ); -} - -// -// Create a mutable copy of another launcher configuration structure. -// -FoobarLauncherConfiguration* foobar_launcher_configuration_copy( FoobarLauncherConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - - FoobarLauncherConfiguration* copy = g_new0( FoobarLauncherConfiguration, 1 ); - copy->width = self->width; - copy->position = self->position; - copy->max_height = self->max_height; - return copy; -} - -// -// Release resources associated with a launcher configuration structure. -// -void foobar_launcher_configuration_free( FoobarLauncherConfiguration* self ) -{ - g_return_if_fail( self != NULL ); - - g_free( self ); -} - -// -// Check if two launcher configuration structures are equal. -// -gboolean foobar_launcher_configuration_equal( - FoobarLauncherConfiguration const* a, - FoobarLauncherConfiguration const* b ) -{ - g_return_val_if_fail( a != NULL, FALSE ); - g_return_val_if_fail( b != NULL, FALSE ); - - if ( a->width != b->width ) { return FALSE; } - if ( a->position != b->position ) { return FALSE; } - if ( a->max_height != b->max_height ) { return FALSE; } - - return TRUE; -} - -// -// Horizontal size of the launcher. -// -gint foobar_launcher_configuration_get_width( FoobarLauncherConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->width; -} - -// -// Offset from the top of the screen. -// -gint foobar_launcher_configuration_get_position( FoobarLauncherConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->position; -} - -// -// Maximum allowed height for the launcher before scrolling is enabled. -// -gint foobar_launcher_configuration_get_max_height( FoobarLauncherConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->max_height; -} - -// -// Horizontal size of the launcher. -// -void foobar_launcher_configuration_set_width( - FoobarLauncherConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->width = value; -} - -// -// Offset from the top of the screen. -// -void foobar_launcher_configuration_set_position( - FoobarLauncherConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->position = value; -} - -// -// Maximum allowed height for the launcher before scrolling is enabled. -// -void foobar_launcher_configuration_set_max_height( - FoobarLauncherConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->max_height = value; -} - -// -// Populate a launcher configuration structure from the "launcher" section of a keyfile. -// -void foobar_launcher_configuration_load( - FoobarLauncherConfiguration* self, - GKeyFile* file ) -{ - gint width; - if ( try_get_int_value( file, "launcher", "width", VALIDATE_POSITIVE, &width ) ) - { - foobar_launcher_configuration_set_width( self, width ); - } - - gint position; - if ( try_get_int_value( file, "launcher", "position", VALIDATE_NON_NEGATIVE, &position ) ) - { - foobar_launcher_configuration_set_position( self, position ); - } - - gint max_height; - if ( try_get_int_value( file, "launcher", "max-height", VALIDATE_POSITIVE, &max_height ) ) - { - foobar_launcher_configuration_set_position( self, max_height ); - } -} - -// -// Store a launcher configuration structure in the "launcher" section of a keyfile. -// -void foobar_launcher_configuration_store( - FoobarLauncherConfiguration const* self, - GKeyFile* file ) -{ - gint width = foobar_launcher_configuration_get_width( self ); - g_key_file_set_integer( file, "launcher", "width", width ); - - gint position = foobar_launcher_configuration_get_position( self ); - g_key_file_set_integer( file, "launcher", "position", position ); - g_key_file_set_comment( - file, - "launcher", - "position", - " Offset from the top of the screen.", - NULL ); - - gint max_height = foobar_launcher_configuration_get_max_height( self ); - g_key_file_set_integer( file, "launcher", "max-height", max_height ); - g_key_file_set_comment( - file, - "launcher", - "max-height", - " Maximum allowed height for the launcher before scrolling is enabled.", - NULL ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Control Center Configuration -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a default control center configuration structure. -// -FoobarControlCenterConfiguration* foobar_control_center_configuration_new( void ) -{ - return foobar_control_center_configuration_copy( &default_control_center_configuration ); -} - -// -// Create a mutable copy of another control center configuration structure. -// -FoobarControlCenterConfiguration* foobar_control_center_configuration_copy( FoobarControlCenterConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - - FoobarControlCenterConfiguration* copy = g_new0( FoobarControlCenterConfiguration, 1 ); - copy->width = self->width; - copy->height = self->height; - copy->position = self->position; - copy->offset = self->offset; - copy->padding = self->padding; - copy->spacing = self->spacing; - copy->orientation = self->orientation; - copy->alignment = self->alignment; - copy->rows = g_new( FoobarControlCenterRow, self->rows_count ); - copy->rows_count = self->rows_count; - memcpy( copy->rows, self->rows, sizeof( *copy->rows ) * copy->rows_count ); - return copy; -} - -// -// Release resources associated with a control center configuration structure. -// -void foobar_control_center_configuration_free( FoobarControlCenterConfiguration* self ) -{ - g_return_if_fail( self != NULL ); - - g_free( self->rows ); - g_free( self ); -} - -// -// Check if two control center configuration structures are equal. -// -gboolean foobar_control_center_configuration_equal( - FoobarControlCenterConfiguration const* a, - FoobarControlCenterConfiguration const* b ) -{ - g_return_val_if_fail( a != NULL, FALSE ); - g_return_val_if_fail( b != NULL, FALSE ); - - if ( a->width != b->width ) { return FALSE; } - if ( a->height != b->height ) { return FALSE; } - if ( a->position != b->position ) { return FALSE; } - if ( a->offset != b->offset ) { return FALSE; } - if ( a->padding != b->padding ) { return FALSE; } - if ( a->spacing != b->spacing ) { return FALSE; } - if ( a->orientation != b->orientation ) { return FALSE; } - if ( a->alignment != b->alignment ) { return FALSE; } - if ( a->rows_count != b->rows_count ) { return FALSE; } - if ( memcmp( a->rows, b->rows, sizeof( *a->rows ) * a->rows_count ) ) { return FALSE; } - - return TRUE; -} - -// -// Horizontal size of the control center. -// -gint foobar_control_center_configuration_get_width( FoobarControlCenterConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->width; -} - -// -// Vertical size of the control center. -// -gint foobar_control_center_configuration_get_height( FoobarControlCenterConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->height; -} - -// -// The screen edge which the control center should be attached to. -// -FoobarScreenEdge foobar_control_center_configuration_get_position( FoobarControlCenterConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->position; -} - -// -// Offset from the attached screen edge. -// -gint foobar_control_center_configuration_get_offset( FoobarControlCenterConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->offset; -} - -// -// Spacing between the edges of the control center and its items. -// -gint foobar_control_center_configuration_get_padding( FoobarControlCenterConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->padding; -} - -// -// Spacing between the items. -// -gint foobar_control_center_configuration_get_spacing( FoobarControlCenterConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->spacing; -} - -// -// The orientation used to arrange the controls and notifications sections. -// -FoobarOrientation foobar_control_center_configuration_get_orientation( FoobarControlCenterConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->orientation; -} - -// -// Alignment along the attached screen edge, or "fill". -// -FoobarControlCenterAlignment foobar_control_center_configuration_get_alignment( FoobarControlCenterConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->alignment; -} - -// -// Ordered list of rows to display in the controls section. -// -FoobarControlCenterRow const* foobar_control_center_configuration_get_rows( - FoobarControlCenterConfiguration const* self, - gsize* out_count ) -{ - g_return_val_if_fail( self != NULL, 0 ); - g_return_val_if_fail( out_count != NULL, NULL ); - - *out_count = self->rows_count; - return self->rows; -} - -// -// Horizontal size of the control center. -// -void foobar_control_center_configuration_set_width( - FoobarControlCenterConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->width = value; -} - -// -// Vertical size of the control center. -// -void foobar_control_center_configuration_set_height( - FoobarControlCenterConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->height = value; -} - -// -// The screen edge which the control center should be attached to. -// -void foobar_control_center_configuration_set_position( - FoobarControlCenterConfiguration* self, - FoobarScreenEdge value ) -{ - g_return_if_fail( self != NULL ); - self->position = value; -} - -// -// Offset from the attached screen edge. -// -void foobar_control_center_configuration_set_offset( - FoobarControlCenterConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->offset = value; -} - -// -// Spacing between the edges of the control center and its items. -// -void foobar_control_center_configuration_set_padding( - FoobarControlCenterConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->padding = value; -} - -// -// Spacing between the items. -// -void foobar_control_center_configuration_set_spacing( - FoobarControlCenterConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->spacing = value; -} - -// -// The orientation used to arrange the controls and notifications sections. -// -void foobar_control_center_configuration_set_orientation( - FoobarControlCenterConfiguration* self, - FoobarOrientation value ) -{ - g_return_if_fail( self != NULL ); - self->orientation = value; -} - -// -// Alignment along the attached screen edge, or "fill". -// -void foobar_control_center_configuration_set_alignment( - FoobarControlCenterConfiguration* self, - FoobarControlCenterAlignment value ) -{ - g_return_if_fail( self != NULL ); - self->alignment = value; -} - -// -// Ordered list of rows to display in the controls section. -// -void foobar_control_center_configuration_set_rows( - FoobarControlCenterConfiguration* self, - FoobarControlCenterRow const* value, - gsize value_count ) -{ - g_return_if_fail( self != NULL ); - g_return_if_fail( value != NULL || value_count == 0 ); - - g_free( self->rows ); - self->rows = g_new( FoobarControlCenterRow, value_count ); - self->rows_count = value_count; - memcpy( self->rows, value, sizeof( *self->rows ) * self->rows_count ); -} - -// -// Populate a control center configuration structure from the "control-center" section of a keyfile. -// -void foobar_control_center_configuration_load( - FoobarControlCenterConfiguration* self, - GKeyFile* file ) -{ - gint width; - if ( try_get_int_value( file, "control-center", "width", VALIDATE_POSITIVE, &width ) ) - { - foobar_control_center_configuration_set_width( self, width ); - } - - gint height; - if ( try_get_int_value( file, "control-center", "height", VALIDATE_POSITIVE, &height ) ) - { - foobar_control_center_configuration_set_height( self, height ); - } - - gint position; - if ( try_get_enum_value( file, "control-center", "position", FOOBAR_TYPE_SCREEN_EDGE, VALIDATE_NONE, &position ) ) - { - foobar_control_center_configuration_set_position( self, position ); - } - - gint offset; - if ( try_get_int_value( file, "control-center", "offset", VALIDATE_NON_NEGATIVE, &offset ) ) - { - foobar_control_center_configuration_set_offset( self, offset ); - } - - gint padding; - if ( try_get_int_value( file, "control-center", "padding", VALIDATE_NON_NEGATIVE, &padding ) ) - { - foobar_control_center_configuration_set_padding( self, padding ); - } - - gint spacing; - if ( try_get_int_value( file, "control-center", "spacing", VALIDATE_NON_NEGATIVE, &spacing ) ) - { - foobar_control_center_configuration_set_spacing( self, spacing ); - } - - gint orientation; - if ( try_get_enum_value( file, "control-center", "orientation", FOOBAR_TYPE_ORIENTATION, VALIDATE_NONE, &orientation ) ) - { - foobar_control_center_configuration_set_orientation( self, orientation ); - } - - gint alignment; - if ( try_get_enum_value( file, "control-center", "alignment", FOOBAR_TYPE_CONTROL_CENTER_ALIGNMENT, VALIDATE_NONE, &alignment ) ) - { - foobar_control_center_configuration_set_alignment( self, alignment ); - } - - g_autofree gpointer rows = NULL; - gsize rows_count; - if ( try_get_enum_list_value( file, "control-center", "rows", FOOBAR_TYPE_CONTROL_CENTER_ROW, VALIDATE_DISTINCT, &rows, &rows_count ) ) - { - foobar_control_center_configuration_set_rows( self, rows, rows_count ); - } -} - -// -// Store a control center configuration structure in the "control-center" section of a keyfile. -// -void foobar_control_center_configuration_store( - FoobarControlCenterConfiguration const* self, - GKeyFile* file ) -{ - gint width = foobar_control_center_configuration_get_width( self ); - g_key_file_set_integer( file, "control-center", "width", width ); - - gint height = foobar_control_center_configuration_get_height( self ); - g_key_file_set_integer( file, "control-center", "height", height ); - - FoobarScreenEdge position = foobar_control_center_configuration_get_position( self ); - g_key_file_set_string( file, "control-center", "position", enum_value_to_string( FOOBAR_TYPE_SCREEN_EDGE, position ) ); - g_key_file_set_comment( - file, - "control-center", - "position", - " The screen edge which the control center should be attached to.", - NULL ); - - gint offset = foobar_control_center_configuration_get_offset( self ); - g_key_file_set_integer( file, "control-center", "offset", offset ); - g_key_file_set_comment( - file, - "control-center", - "offset", - " Offset from the attached screen edge.", - NULL ); - - gint padding = foobar_control_center_configuration_get_padding( self ); - g_key_file_set_integer( file, "control-center", "padding", padding ); - g_key_file_set_comment( - file, - "control-center", - "padding", - " Spacing between the edges of the control center and its items.", - NULL ); - - gint spacing = foobar_control_center_configuration_get_spacing( self ); - g_key_file_set_integer( file, "control-center", "spacing", spacing ); - g_key_file_set_comment( - file, - "control-center", - "spacing", - " Spacing between the items.", - NULL ); - - FoobarOrientation orientation = foobar_control_center_configuration_get_orientation( self ); - g_key_file_set_string( file, "control-center", "orientation", enum_value_to_string( FOOBAR_TYPE_ORIENTATION, orientation ) ); - g_key_file_set_comment( - file, - "control-center", - "orientation", - " The orientation used to arrange the controls and notifications sections.", - NULL ); - - FoobarControlCenterAlignment alignment = foobar_control_center_configuration_get_alignment( self ); - g_key_file_set_string( file, "control-center", "alignment", enum_value_to_string( FOOBAR_TYPE_CONTROL_CENTER_ALIGNMENT, alignment ) ); - g_key_file_set_comment( - file, - "control-center", - "alignment", - " Alignment along the attached screen edge, or \"fill\".", - NULL ); - - gsize rows_count; - FoobarControlCenterRow const* rows = foobar_control_center_configuration_get_rows( self, &rows_count ); - g_autofree gchar const** rows_str = enum_list_to_string( FOOBAR_TYPE_CONTROL_CENTER_ROW, rows, rows_count ); - g_key_file_set_string_list( file, "control-center", "rows", rows_str, rows_count ); - g_key_file_set_comment( - file, - "control-center", - "rows", - " Ordered list of rows to display in the controls section.", - NULL ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Notification Configuration -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a default notification configuration structure. -// -FoobarNotificationConfiguration* foobar_notification_configuration_new( void ) -{ - return foobar_notification_configuration_copy( &default_notification_configuration ); -} - -// -// Create a mutable copy of another notification configuration structure. -// -FoobarNotificationConfiguration* foobar_notification_configuration_copy( FoobarNotificationConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - - FoobarNotificationConfiguration* copy = g_new0( FoobarNotificationConfiguration, 1 ); - copy->width = self->width; - copy->min_height = self->min_height; - copy->spacing = self->spacing; - copy->close_button_inset = self->close_button_inset; - copy->time_format = g_strdup( self->time_format ); - return copy; -} - -// -// Release resources associated with a notification configuration structure. -// -void foobar_notification_configuration_free( FoobarNotificationConfiguration* self ) -{ - g_return_if_fail( self != NULL ); - - g_free( self->time_format ); - g_free( self ); -} - -// -// Check if two notification configuration structures are equal. -// -gboolean foobar_notification_configuration_equal( - FoobarNotificationConfiguration const* a, - FoobarNotificationConfiguration const* b ) -{ - g_return_val_if_fail( a != NULL, FALSE ); - g_return_val_if_fail( b != NULL, FALSE ); - - if ( a->width != b->width ) { return FALSE; } - if ( a->min_height != b->min_height ) { return FALSE; } - if ( a->spacing != b->spacing ) { return FALSE; } - if ( a->close_button_inset != b->close_button_inset ) { return FALSE; } - if ( g_strcmp0( a->time_format, b->time_format ) ) { return FALSE; } - - return TRUE; -} - -// -// Horizontal size of notifications in the notification area. -// -gint foobar_notification_configuration_get_width( FoobarNotificationConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->width; -} - -// -// Minimum height for each notification. -// -gint foobar_notification_configuration_get_min_height( FoobarNotificationConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->min_height; -} - -// -// Spacing between notifications and from the screen edges. -// -gint foobar_notification_configuration_get_spacing( FoobarNotificationConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->spacing; -} - -// -// Inset of the close button within the notification (may be negative). -// -gint foobar_notification_configuration_get_close_button_inset( FoobarNotificationConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, 0 ); - return self->close_button_inset; -} - -// -// The time format string as used by g_date_time_format. -// -gchar const* foobar_notification_configuration_get_time_format( FoobarNotificationConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - return self->time_format; -} - -// -// Horizontal size of notifications in the notification area. -// -void foobar_notification_configuration_set_width( - FoobarNotificationConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->width = value; -} - -// -// Minimum height for each notification. -// -void foobar_notification_configuration_set_min_height( - FoobarNotificationConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->min_height = value; -} - -// -// Spacing between notifications and from the screen edges. -// -void foobar_notification_configuration_set_spacing( - FoobarNotificationConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->spacing = value; -} - -// -// Inset of the close button within the notification (may be negative). -// -void foobar_notification_configuration_set_close_button_inset( - FoobarNotificationConfiguration* self, - gint value ) -{ - g_return_if_fail( self != NULL ); - self->close_button_inset = value; -} - -// -// The time format string as used by g_date_time_format. -// -void foobar_notification_configuration_set_time_format( - FoobarNotificationConfiguration* self, - gchar const* value ) -{ - g_return_if_fail( self != NULL ); - g_return_if_fail( value != NULL ); - - g_free( self->time_format ); - self->time_format = g_strdup( value ); -} - -// -// Populate a notification configuration structure from the "notifications" section of a keyfile. -// -void foobar_notification_configuration_load( - FoobarNotificationConfiguration* self, - GKeyFile* file ) -{ - gint width; - if ( try_get_int_value( file, "notifications", "width", VALIDATE_POSITIVE, &width ) ) - { - foobar_notification_configuration_set_width( self, width ); - } - - gint min_height; - if ( try_get_int_value( file, "notifications", "min-height", VALIDATE_NON_NEGATIVE, &min_height ) ) - { - foobar_notification_configuration_set_min_height( self, min_height ); - } - - gint spacing; - if ( try_get_int_value( file, "notifications", "spacing", VALIDATE_NON_NEGATIVE, &spacing ) ) - { - foobar_notification_configuration_set_spacing( self, spacing ); - } - - gint close_button_inset; - if ( try_get_int_value( file, "notifications", "close-button-inset", VALIDATE_NONE, &close_button_inset ) ) - { - foobar_notification_configuration_set_close_button_inset( self, close_button_inset ); - } - - g_autofree gchar* time_format = NULL; - if ( try_get_string_value( file, "notifications", "time-format", VALIDATE_NONE, &time_format ) ) - { - foobar_notification_configuration_set_time_format( self, time_format ); - } -} - -// -// Store a notification configuration structure in the "notifications" section of a keyfile. -// -void foobar_notification_configuration_store( - FoobarNotificationConfiguration const* self, - GKeyFile* file ) -{ - gint width = foobar_notification_configuration_get_width( self ); - g_key_file_set_integer( file, "notifications", "width", width ); - - gint min_height = foobar_notification_configuration_get_min_height( self ); - g_key_file_set_integer( file, "notifications", "min-height", min_height ); - g_key_file_set_comment( - file, - "notifications", - "min-height", - " Minimum height for each notification.", - NULL ); - - gint spacing = foobar_notification_configuration_get_spacing( self ); - g_key_file_set_integer( file, "notifications", "spacing", spacing ); - g_key_file_set_comment( - file, - "notifications", - "spacing", - " Spacing between notifications and from the screen edges.", - NULL ); - - gint close_button_inset = foobar_notification_configuration_get_close_button_inset( self ); - g_key_file_set_integer( file, "notifications", "close-button-inset", close_button_inset ); - g_key_file_set_comment( - file, - "notifications", - "close-button-inset", - " Inset of the close button within the notification (may be negative).", - NULL ); - - gchar const* time_format = foobar_notification_configuration_get_time_format( self ); - g_key_file_set_string( file, "notifications", "time-format", time_format ); - g_key_file_set_comment( - file, - "notifications", - "time-format", - " The time format string as used by g_date_time_format.", - NULL ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Configuration -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a default configuration structure. -// -FoobarConfiguration* foobar_configuration_new( void ) -{ - return foobar_configuration_copy( &default_configuration ); -} - -// -// Create a mutable copy of another configuration structure. -// -FoobarConfiguration* foobar_configuration_copy( FoobarConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - - FoobarConfiguration* copy = g_new0( FoobarConfiguration, 1 ); - copy->general = foobar_general_configuration_copy( self->general ); - copy->panel = foobar_panel_configuration_copy( self->panel ); - copy->launcher = foobar_launcher_configuration_copy( self->launcher ); - copy->control_center = foobar_control_center_configuration_copy( self->control_center ); - copy->notifications = foobar_notification_configuration_copy( self->notifications ); - return copy; -} - -// -// Release resources associated with a configuration structure. -// -void foobar_configuration_free( FoobarConfiguration* self ) -{ - g_return_if_fail( self != NULL ); - - foobar_general_configuration_free( self->general ); - foobar_panel_configuration_free( self->panel ); - foobar_launcher_configuration_free( self->launcher ); - foobar_control_center_configuration_free( self->control_center ); - foobar_notification_configuration_free( self->notifications ); - g_free( self ); -} - -// -// Check if two configuration structures are equal. -// -gboolean foobar_configuration_equal( - FoobarConfiguration const* a, - FoobarConfiguration const* b ) -{ - g_return_val_if_fail( a != NULL, FALSE ); - g_return_val_if_fail( b != NULL, FALSE ); - - if ( !foobar_general_configuration_equal( a->general, b->general ) ) { return FALSE; } - if ( !foobar_panel_configuration_equal( a->panel, b->panel ) ) { return FALSE; } - if ( !foobar_launcher_configuration_equal( a->launcher, b->launcher ) ) { return FALSE; } - if ( !foobar_control_center_configuration_equal( a->control_center, b->control_center ) ) { return FALSE; } - if ( !foobar_notification_configuration_equal( a->notifications, b->notifications ) ) { return FALSE; } - - return TRUE; -} - -// -// Get the general application configuration. -// -FoobarGeneralConfiguration const* foobar_configuration_get_general( FoobarConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - return self->general; -} - -// -// Get a mutable reference to the general application configuration. -// -FoobarGeneralConfiguration* foobar_configuration_get_general_mut( FoobarConfiguration* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - return self->general; -} - -// -// Get the panel configuration. -// -FoobarPanelConfiguration const* foobar_configuration_get_panel( FoobarConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - return self->panel; -} - -// -// Get a mutable reference to the panel configuration. -// -FoobarPanelConfiguration* foobar_configuration_get_panel_mut( FoobarConfiguration* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - return self->panel; -} - -// -// Get the launcher configuration. -// -FoobarLauncherConfiguration const* foobar_configuration_get_launcher( FoobarConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - return self->launcher; -} - -// -// Get a mutable reference to the launcher configuration. -// -FoobarLauncherConfiguration* foobar_configuration_get_launcher_mut( FoobarConfiguration* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - return self->launcher; -} - -// -// Get the control center configuration. -// -FoobarControlCenterConfiguration const* foobar_configuration_get_control_center( FoobarConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - return self->control_center; -} - -// -// Get a mutable reference to the control center configuration. -// -FoobarControlCenterConfiguration* foobar_configuration_get_control_center_mut( FoobarConfiguration* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - return self->control_center; -} - -// -// Get the notification configuration. -// -FoobarNotificationConfiguration const* foobar_configuration_get_notifications( FoobarConfiguration const* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - return self->notifications; -} - -// -// Get a mutable reference to the notification configuration. -// -FoobarNotificationConfiguration* foobar_configuration_get_notifications_mut( FoobarConfiguration* self ) -{ - g_return_val_if_fail( self != NULL, NULL ); - return self->notifications; -} - -// -// Populate a configuration structure from a keyfile. -// -void foobar_configuration_load( - FoobarConfiguration* self, - GKeyFile* file ) -{ - foobar_general_configuration_load( foobar_configuration_get_general_mut( self ), file ); - foobar_panel_configuration_load( foobar_configuration_get_panel_mut( self ), file ); - foobar_launcher_configuration_load( foobar_configuration_get_launcher_mut( self ), file ); - foobar_control_center_configuration_load( foobar_configuration_get_control_center_mut( self ), file ); - foobar_notification_configuration_load( foobar_configuration_get_notifications_mut( self ), file ); -} - -// -// Store a configuration structure in a keyfile. -// -void foobar_configuration_store( - FoobarConfiguration const* self, - GKeyFile* file ) -{ - foobar_general_configuration_store( foobar_configuration_get_general( self ), file ); - foobar_panel_configuration_store( foobar_configuration_get_panel( self ), file ); - foobar_launcher_configuration_store( foobar_configuration_get_launcher( self ), file ); - foobar_control_center_configuration_store( foobar_configuration_get_control_center( self ), file ); - foobar_notification_configuration_store( foobar_configuration_get_notifications( self ), file ); -} - -// -// Populate a configuration structure from a keyfile at the given path. -// -void foobar_configuration_load_from_file( - FoobarConfiguration* self, - gchar const* path ) -{ - g_autoptr( GKeyFile ) file = g_key_file_new( ); - g_autoptr( GError ) error = NULL; - if ( !g_key_file_load_from_file( file, path, G_KEY_FILE_NONE, &error ) ) - { - CONFIGURATION_WARNING( "%s", error->message ); - return; - } - foobar_configuration_load( self, file ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Service Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the configuration service. -// -void foobar_configuration_service_class_init( FoobarConfigurationServiceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_configuration_service_get_property; - object_klass->finalize = foobar_configuration_service_finalize; - - props[PROP_CURRENT] = g_param_spec_boxed( - "current", - "Current", - "The current configuration state.", - FOOBAR_TYPE_CONFIGURATION, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for the configuration service. -// -void foobar_configuration_service_init( FoobarConfigurationService* self ) -{ - self->path = g_strdup_printf( "%s/foobar.conf", g_get_user_config_dir( ) ); - self->current = foobar_configuration_new( ); - g_return_if_fail( self->path != NULL ); - - // Ensure that the file at path exists. - - if ( g_file_test( self->path, G_FILE_TEST_EXISTS ) ) - { - foobar_configuration_load_from_file( self->current, self->path ); - } - else - { - g_info( "Config file does not exist -- falling back to default config and creating a copy of it." ); - g_autoptr( GKeyFile ) file = g_key_file_new( ); - foobar_configuration_store( self->current, file ); - g_key_file_save_to_file( file, self->path, NULL ); - } - - // Monitor the config file. - - self->monitor = foobar_configuration_service_create_monitor( self->path ); - if ( self->monitor ) - { - self->changed_handler_id = g_signal_connect( - self->monitor, - "changed", - G_CALLBACK( foobar_configuration_service_handle_changed ), - self ); - } - - // In case the config is symlinked, also watch the actual file for content changes. - - self->actual_path = realpath( self->path, NULL ); - if ( self->actual_path && g_strcmp0( self->actual_path, self->path ) ) - { - self->actual_monitor = foobar_configuration_service_create_monitor( self->actual_path ); - if ( self->actual_monitor ) - { - self->actual_changed_handler_id = g_signal_connect( - self->actual_monitor, - "changed", - G_CALLBACK( foobar_configuration_service_handle_actual_changed ), - self ); - } - } -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_configuration_service_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarConfigurationService* self = (FoobarConfigurationService*)object; - - switch ( prop_id ) - { - case PROP_CURRENT: - g_value_set_boxed( value, foobar_configuration_service_get_current( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for the configuration service. -// -void foobar_configuration_service_finalize( GObject* object ) -{ - FoobarConfigurationService* self = (FoobarConfigurationService*)object; - - g_clear_handle_id( &self->update_source_id, g_source_remove ); - g_clear_signal_handler( &self->changed_handler_id, self->monitor ); - g_clear_signal_handler( &self->actual_changed_handler_id, self->actual_monitor ); - g_clear_object( &self->monitor ); - g_clear_object( &self->actual_monitor ); - g_clear_pointer( &self->current, foobar_configuration_free ); - g_clear_pointer( &self->path, g_free ); - g_clear_pointer( &self->actual_path, free ); - - G_OBJECT_CLASS( foobar_configuration_service_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new configuration service instance. -// -FoobarConfigurationService* foobar_configuration_service_new( void ) -{ - return g_object_new( FOOBAR_TYPE_CONFIGURATION_SERVICE, NULL ); -} - -// -// Get the current configuration state. -// -FoobarConfiguration const* foobar_configuration_service_get_current( FoobarConfigurationService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONFIGURATION_SERVICE( self ), NULL ); - return self->current; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called when the file at self->path has changed. -// -void foobar_configuration_service_handle_changed( - GFileMonitor* monitor, - GFile* file, - GFile* other_file, - GFileMonitorEvent event_type, - gpointer userdata ) -{ - (void)monitor; - (void)file; - (void)other_file; - FoobarConfigurationService* self = (FoobarConfigurationService*)userdata; - - // If this file is a symlink, the actual path might have changed. - - char* actual_path = realpath( self->path, NULL ); - if ( g_strcmp0( self->actual_path, actual_path ) ) - { - free(self->actual_path); - self->actual_path = g_steal_pointer( &actual_path ); - - // Recreate the file monitor. - - g_clear_signal_handler( &self->actual_changed_handler_id, self->actual_monitor ); - g_clear_object( &self->actual_monitor ); - - if ( self->actual_path && g_strcmp0( self->actual_path, self->path ) ) - { - self->actual_monitor = foobar_configuration_service_create_monitor( self->actual_path ); - if ( self->actual_monitor ) - { - self->actual_changed_handler_id = g_signal_connect( - self->actual_monitor, - "changed", - G_CALLBACK( foobar_configuration_service_handle_actual_changed ), - self ); - } - } - } - free(actual_path); - - // Handle actual file changes. - - if ( event_type == G_FILE_MONITOR_EVENT_CREATED || event_type == G_FILE_MONITOR_EVENT_CHANGED ) - { - foobar_configuration_service_update( self ); - } -} - -// -// Called when the actual file (the one at self->actual_path) behind the symlink has changed. -// -void foobar_configuration_service_handle_actual_changed( - GFileMonitor* monitor, - GFile* file, - GFile* other_file, - GFileMonitorEvent event_type, - gpointer userdata ) -{ - (void)monitor; - (void)file; - (void)other_file; - FoobarConfigurationService* self = (FoobarConfigurationService*)userdata; - - if ( event_type == G_FILE_MONITOR_EVENT_CREATED || event_type == G_FILE_MONITOR_EVENT_CHANGED ) - { - foobar_configuration_service_update( self ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Methods -// --------------------------------------------------------------------------------------------------------------------- - -// -// Schedule a configuration file reload, used to coalesce change events. -// -void foobar_configuration_service_update( FoobarConfigurationService* self ) -{ - if ( !self->update_source_id ) - { - self->update_source_id = g_timeout_add( - UPDATE_APPLY_DELAY, - foobar_configuration_service_update_cb, - self ); - } -} - -// -// Reload the configuration file. -// -gboolean foobar_configuration_service_update_cb( gpointer userdata ) -{ - FoobarConfigurationService* self = (FoobarConfigurationService*)userdata; - - FoobarConfiguration* updated = foobar_configuration_new( ); - foobar_configuration_load_from_file( updated, self->path ); - if ( !foobar_configuration_equal( self->current, updated ) ) - { - foobar_configuration_free( self->current ); - self->current = g_steal_pointer( &updated ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CURRENT] ); - g_info( "Config reloaded." ); - } - g_clear_pointer( &updated, foobar_configuration_free ); - - self->update_source_id = 0; - return G_SOURCE_REMOVE; -} - -// -// Create a new file monitor, logging a warning on error. -// -GFileMonitor* foobar_configuration_service_create_monitor( gchar const* path ) -{ - g_autoptr( GError ) error = NULL; - g_autoptr( GFile ) file = g_file_new_for_path( path ); - GFileMonitor* monitor = g_file_monitor( file, G_FILE_MONITOR_NONE, NULL, &error ); - if ( !monitor ) - { - g_warning( "Unable to monitor config file: %s", error->message ); - } - - return monitor; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Parsing Helpers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Get and validate a string value from a keyfile. *out should always be freed, even if FALSE is returned. -// -gboolean try_get_string_value( - GKeyFile* file, - gchar const* group, - gchar const* key, - ValidationFlags validation, - gchar** out ) -{ - if ( !g_key_file_has_key( file, group, key, NULL ) ) { return FALSE; } - - g_autoptr( GError ) error = NULL; - *out = g_key_file_get_string( file, group, key, &error ); - if ( !*out ) - { - CONFIGURATION_WARNING( "%s:%s: %s", group, key, error->message ); - return FALSE; - } - - if ( validation & VALIDATE_FILE_URL ) - { - g_autoptr( GFile ) test_file = g_file_new_for_uri( *out ); - if ( !g_file_query_exists( test_file, NULL ) ) - { - CONFIGURATION_WARNING( "%s:%s: A file with the URL \"%s\" does not exist.", group, key, *out ); - return FALSE; - } - } - - return TRUE; -} - -// -// Get and validate a list of string values from a keyfile. *out should always be freed, even if FALSE is returned. -// -gboolean try_get_string_list_value( - GKeyFile* file, - gchar const* group, - gchar const* key, - ValidationFlags validation, - GStrv* out, - gsize* out_count ) -{ - if ( !g_key_file_has_key( file, group, key, NULL ) ) { return FALSE; } - - g_autoptr( GError ) error = NULL; - *out = g_key_file_get_string_list( file, group, key, out_count, &error ); - if ( !*out ) - { - CONFIGURATION_WARNING( "%s:%s: %s", group, key, error->message ); - return FALSE; - } - - if ( validation & VALIDATE_DISTINCT ) - { - g_autoptr( GHashTable ) items = g_hash_table_new( g_str_hash, g_str_equal ); - for ( gchar** it = *out; *it; ++it ) - { - if ( !g_hash_table_add( items, *it ) ) - { - CONFIGURATION_WARNING( "%s:%s: Duplicate value \"%s\" not allowed.", group, key, *it ); - return FALSE; - } - } - } - - return TRUE; -} - -// -// Get and validate an integer value from a keyfile. -// -gboolean try_get_int_value( - GKeyFile* file, - gchar const* group, - gchar const* key, - ValidationFlags validation, - gint* out ) -{ - if ( !g_key_file_has_key( file, group, key, NULL ) ) { return FALSE; } - - g_autoptr( GError ) error = NULL; - *out = g_key_file_get_integer( file, group, key, &error ); - if ( error ) - { - CONFIGURATION_WARNING( "%s:%s: %s", group, key, error->message ); - return FALSE; - } - - if ( validation & VALIDATE_NON_NEGATIVE ) - { - if ( *out < 0 ) - { - CONFIGURATION_WARNING( "%s:%s: Value is not allowed to be negative.", group, key ); - return FALSE; - } - } - - if ( validation & VALIDATE_NON_ZERO ) - { - if ( *out == 0 ) - { - CONFIGURATION_WARNING( "%s:%s: Value is not allowed to be 0.", group, key ); - return FALSE; - } - } - - return TRUE; -} - -// -// Get and validate an enum value (encoded as a string) from a keyfile. -// -gboolean try_get_enum_value( - GKeyFile* file, - gchar const* group, - gchar const* key, - GType enum_type, - ValidationFlags validation, - gint* out ) -{ - (void)validation; - - g_autofree gchar* str = NULL; - if ( !try_get_string_value( file, group, key, VALIDATE_NONE, &str ) ) { return FALSE; } - - g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type ); - GEnumValue* val = g_enum_get_value_by_nick( enum_klass, str ); - if ( !val ) - { - g_autofree gchar* help_text = enum_format_help_text( enum_type ); - CONFIGURATION_WARNING( "%s:%s: Invalid enum value \"%s\" (allowed values: %s).", group, key, str, help_text ); - return FALSE; - } - - *out = val->value; - return TRUE; -} - -// -// Get and validate a list of enum values (encoded as strings) from a keyfile. *out should always be freed, even if -// FALSE is returned. -// -gboolean try_get_enum_list_value( - GKeyFile* file, - gchar const* group, - gchar const* key, - GType enum_type, - ValidationFlags validation, - gpointer* out, - gsize* out_count ) -{ - g_auto( GStrv ) str_values = NULL; - if ( !try_get_string_list_value( file, group, key, validation, &str_values, out_count ) ) { return FALSE; } - - gint* untyped_out = g_new0( gint, *out_count ); - *out = untyped_out; - - g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type ); - for ( gsize i = 0; i < *out_count; ++i ) - { - GEnumValue* val = g_enum_get_value_by_nick( enum_klass, str_values[i] ); - if ( !val ) - { - g_autofree gchar* help_text = enum_format_help_text( enum_type ); - CONFIGURATION_WARNING( "%s:%s: Invalid enum value \"%s\" (allowed values: %s).", group, key, str_values[i], help_text ); - return FALSE; - } - - untyped_out[i] = val->value; - } - - return TRUE; -} - -// -// Get and validate a boolean value from a keyfile. -// -gboolean try_get_boolean_value( - GKeyFile* file, - gchar const* group, - gchar const* key, - ValidationFlags validation, - gboolean* out ) -{ - (void)validation; - - if ( !g_key_file_has_key( file, group, key, NULL ) ) { return FALSE; } - - g_autoptr( GError ) error = NULL; - *out = g_key_file_get_boolean( file, group, key, &error ); - if ( error ) - { - CONFIGURATION_WARNING( "%s:%s: %s", group, key, error->message ); - return FALSE; - } - - return TRUE; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Serialization -// --------------------------------------------------------------------------------------------------------------------- - -// -// Get the string constant for an enum value. -// -gchar const* enum_value_to_string( - GType enum_type, - gint value ) -{ - g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type ); - GEnumValue* val = g_enum_get_value( enum_klass, value ); - return val->value_nick; -} - -// -// Get a list of string constants for a list of enum values. Only the list itself needs to be freed. -// -gchar const** enum_list_to_string( - GType enum_type, - gconstpointer values, - gsize values_count ) -{ - gint const* untyped_values = values; - gchar const** result = g_new0( gchar const*, values_count ); - - g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type ); - for ( gsize i = 0; i < values_count; ++i ) - { - GEnumValue* val = g_enum_get_value( enum_klass, untyped_values[i] ); - result[i] = val->value_nick; - } - - return result; -} - -// -// Return a comma-separated list of all allowed values for an enum type. The result needs to be freed. -// -gchar* enum_format_help_text( GType enum_type ) -{ - GString* result = g_string_new( NULL ); - g_autoptr( GEnumClass ) enum_klass = g_type_class_ref( enum_type ); - - gboolean is_first = TRUE; - for ( guint i = 0; i < enum_klass->n_values; ++i ) - { - g_string_append_printf( result, is_first ? "\"%s\"" : ", \"%s\"", enum_klass->values[i].value_nick ); - is_first = FALSE; - } - - return g_string_free_and_steal( result ); -} diff --git a/src/services/configuration-service.h b/src/services/configuration-service.h deleted file mode 100644 index 2c1296b..0000000 --- a/src/services/configuration-service.h +++ /dev/null @@ -1,305 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_SCREEN_EDGE foobar_screen_edge_get_type( ) -#define FOOBAR_TYPE_ORIENTATION foobar_orientation_get_type( ) -#define FOOBAR_TYPE_STATUS_ITEM foobar_status_item_get_type( ) -#define FOOBAR_TYPE_PANEL_ITEM_KIND foobar_panel_item_kind_get_type( ) -#define FOOBAR_TYPE_PANEL_ITEM_ACTION foobar_panel_item_action_get_type( ) -#define FOOBAR_TYPE_PANEL_ITEM_POSITION foobar_panel_item_position_get_type( ) -#define FOOBAR_TYPE_CONTROL_CENTER_ROW foobar_control_center_row_get_type( ) -#define FOOBAR_TYPE_CONTROL_CENTER_ALIGNMENT foobar_control_center_alignment_get_type( ) -#define FOOBAR_TYPE_GENERAL_CONFIGURATION foobar_general_configuration_get_type( ) -#define FOOBAR_TYPE_PANEL_ITEM_CONFIGURATION foobar_panel_item_configuration_get_type( ) -#define FOOBAR_TYPE_PANEL_CONFIGURATION foobar_panel_configuration_get_type( ) -#define FOOBAR_TYPE_LAUNCHER_CONFIGURATION foobar_launcher_configuration_get_type( ) -#define FOOBAR_TYPE_CONTROL_CENTER_CONFIGURATION foobar_control_center_configuration_get_type( ) -#define FOOBAR_TYPE_NOTIFICATION_CONFIGURATION foobar_notification_configuration_get_type( ) -#define FOOBAR_TYPE_CONFIGURATION foobar_configuration_get_type( ) -#define FOOBAR_TYPE_CONFIGURATION_SERVICE foobar_configuration_service_get_type( ) - -typedef enum -{ - FOOBAR_SCREEN_EDGE_LEFT, - FOOBAR_SCREEN_EDGE_RIGHT, - FOOBAR_SCREEN_EDGE_TOP, - FOOBAR_SCREEN_EDGE_BOTTOM, -} FoobarScreenEdge; - -GType foobar_screen_edge_get_type( void ); - -typedef enum -{ - FOOBAR_ORIENTATION_HORIZONTAL, - FOOBAR_ORIENTATION_VERTICAL, -} FoobarOrientation; - -GType foobar_orientation_get_type( void ); - -typedef enum -{ - FOOBAR_STATUS_ITEM_NETWORK, - FOOBAR_STATUS_ITEM_BLUETOOTH, - FOOBAR_STATUS_ITEM_BATTERY, - FOOBAR_STATUS_ITEM_BRIGHTNESS, - FOOBAR_STATUS_ITEM_AUDIO, - FOOBAR_STATUS_ITEM_NOTIFICATIONS, -} FoobarStatusItem; - -GType foobar_status_item_get_type( void ); - -typedef enum -{ - FOOBAR_PANEL_ITEM_KIND_ICON, - FOOBAR_PANEL_ITEM_KIND_CLOCK, - FOOBAR_PANEL_ITEM_KIND_WORKSPACES, - FOOBAR_PANEL_ITEM_KIND_STATUS, -} FoobarPanelItemKind; - -GType foobar_panel_item_kind_get_type( void ); - -typedef enum -{ - FOOBAR_PANEL_ITEM_ACTION_NONE, - FOOBAR_PANEL_ITEM_ACTION_LAUNCHER, - FOOBAR_PANEL_ITEM_ACTION_CONTROL_CENTER, -} FoobarPanelItemAction; - -GType foobar_panel_item_action_get_type( void ); - -typedef enum -{ - FOOBAR_PANEL_ITEM_POSITION_START, - FOOBAR_PANEL_ITEM_POSITION_CENTER, - FOOBAR_PANEL_ITEM_POSITION_END, -} FoobarPanelItemPosition; - -GType foobar_panel_item_position_get_type( void ); - -typedef enum -{ - FOOBAR_CONTROL_CENTER_ROW_CONNECTIVITY, - FOOBAR_CONTROL_CENTER_ROW_AUDIO_OUTPUT, - FOOBAR_CONTROL_CENTER_ROW_AUDIO_INPUT, - FOOBAR_CONTROL_CENTER_ROW_BRIGHTNESS, -} FoobarControlCenterRow; - -GType foobar_control_center_row_get_type( void ); - -typedef enum -{ - FOOBAR_CONTROL_CENTER_ALIGNMENT_START, - FOOBAR_CONTROL_CENTER_ALIGNMENT_CENTER, - FOOBAR_CONTROL_CENTER_ALIGNMENT_END, - FOOBAR_CONTROL_CENTER_ALIGNMENT_FILL, -} FoobarControlCenterAlignment; - -GType foobar_control_center_alignment_get_type( void ); - -typedef struct _FoobarGeneralConfiguration FoobarGeneralConfiguration; - -GType foobar_general_configuration_get_type ( void ); -FoobarGeneralConfiguration* foobar_general_configuration_new ( void ); -FoobarGeneralConfiguration* foobar_general_configuration_copy ( FoobarGeneralConfiguration const* self ); -void foobar_general_configuration_free ( FoobarGeneralConfiguration* self ); -gboolean foobar_general_configuration_equal ( FoobarGeneralConfiguration const* a, - FoobarGeneralConfiguration const* b ); -gchar const* foobar_general_configuration_get_stylesheet( FoobarGeneralConfiguration const* self ); -void foobar_general_configuration_set_stylesheet( FoobarGeneralConfiguration* self, - gchar const* value ); - -typedef struct _FoobarPanelItemConfiguration FoobarPanelItemConfiguration; - -GType foobar_panel_item_configuration_get_type ( void ); -FoobarPanelItemConfiguration* foobar_panel_item_configuration_copy ( FoobarPanelItemConfiguration const* self ); -void foobar_panel_item_configuration_free ( FoobarPanelItemConfiguration* self ); -gboolean foobar_panel_item_configuration_equal ( FoobarPanelItemConfiguration const* a, - FoobarPanelItemConfiguration const* b ); -FoobarPanelItemKind foobar_panel_item_configuration_get_kind ( FoobarPanelItemConfiguration const* self ); -gchar const* foobar_panel_item_configuration_get_name ( FoobarPanelItemConfiguration const* self ); -FoobarPanelItemPosition foobar_panel_item_configuration_get_position( FoobarPanelItemConfiguration const* self ); -FoobarPanelItemAction foobar_panel_item_configuration_get_action ( FoobarPanelItemConfiguration const* self ); -void foobar_panel_item_configuration_set_name ( FoobarPanelItemConfiguration* self, - gchar const* value ); -void foobar_panel_item_configuration_set_position( FoobarPanelItemConfiguration* self, - FoobarPanelItemPosition value ); -void foobar_panel_item_configuration_set_action ( FoobarPanelItemConfiguration* self, - FoobarPanelItemAction value ); - -FoobarPanelItemConfiguration* foobar_panel_item_icon_configuration_new ( void ); -gchar const* foobar_panel_item_icon_configuration_get_icon_name( FoobarPanelItemConfiguration const* self ); -void foobar_panel_item_icon_configuration_set_icon_name( FoobarPanelItemConfiguration* self, - gchar const* value ); - -FoobarPanelItemConfiguration* foobar_panel_item_clock_configuration_new ( void ); -gchar const* foobar_panel_item_clock_configuration_get_format( FoobarPanelItemConfiguration const* self ); -void foobar_panel_item_clock_configuration_set_format( FoobarPanelItemConfiguration* self, - gchar const* value ); - -FoobarPanelItemConfiguration* foobar_panel_item_workspaces_configuration_new ( void ); -gint foobar_panel_item_workspaces_configuration_get_button_size( FoobarPanelItemConfiguration const* self ); -gint foobar_panel_item_workspaces_configuration_get_spacing ( FoobarPanelItemConfiguration const* self ); -void foobar_panel_item_workspaces_configuration_set_button_size( FoobarPanelItemConfiguration* self, - gint value ); -void foobar_panel_item_workspaces_configuration_set_spacing ( FoobarPanelItemConfiguration* self, - gint value ); - -FoobarPanelItemConfiguration* foobar_panel_item_status_configuration_new ( void ); -FoobarStatusItem const* foobar_panel_item_status_configuration_get_items ( FoobarPanelItemConfiguration const* self, - gsize* out_count ); -gint foobar_panel_item_status_configuration_get_spacing ( FoobarPanelItemConfiguration const* self ); -gboolean foobar_panel_item_status_configuration_get_show_labels ( FoobarPanelItemConfiguration const* self ); -gboolean foobar_panel_item_status_configuration_get_enable_scrolling( FoobarPanelItemConfiguration const* self ); -void foobar_panel_item_status_configuration_set_items ( FoobarPanelItemConfiguration* self, - FoobarStatusItem const* value, - gsize value_count ); -void foobar_panel_item_status_configuration_set_spacing ( FoobarPanelItemConfiguration* self, - gint value ); -void foobar_panel_item_status_configuration_set_show_labels ( FoobarPanelItemConfiguration* self, - gboolean value ); -void foobar_panel_item_status_configuration_set_enable_scrolling( FoobarPanelItemConfiguration* self, - gboolean value ); - -typedef struct _FoobarPanelConfiguration FoobarPanelConfiguration; - -GType foobar_panel_configuration_get_type ( void ); -FoobarPanelConfiguration* foobar_panel_configuration_new ( void ); -FoobarPanelConfiguration* foobar_panel_configuration_copy ( FoobarPanelConfiguration const* self ); -void foobar_panel_configuration_free ( FoobarPanelConfiguration* self ); -gboolean foobar_panel_configuration_equal ( FoobarPanelConfiguration const* a, - FoobarPanelConfiguration const* b ); -FoobarScreenEdge foobar_panel_configuration_get_position ( FoobarPanelConfiguration const* self ); -gint foobar_panel_configuration_get_margin ( FoobarPanelConfiguration const* self ); -gint foobar_panel_configuration_get_padding ( FoobarPanelConfiguration const* self ); -gint foobar_panel_configuration_get_size ( FoobarPanelConfiguration const* self ); -gint foobar_panel_configuration_get_spacing ( FoobarPanelConfiguration const* self ); -gboolean foobar_panel_configuration_get_multi_monitor( FoobarPanelConfiguration const* self ); -FoobarPanelItemConfiguration const* const* foobar_panel_configuration_get_items ( FoobarPanelConfiguration const* self, - gsize* out_count ); -FoobarPanelItemConfiguration* const* foobar_panel_configuration_get_items_mut ( FoobarPanelConfiguration* self, - gsize* out_count ); -void foobar_panel_configuration_set_position ( FoobarPanelConfiguration* self, - FoobarScreenEdge value ); -void foobar_panel_configuration_set_margin ( FoobarPanelConfiguration* self, - gint value ); -void foobar_panel_configuration_set_padding ( FoobarPanelConfiguration* self, - gint value ); -void foobar_panel_configuration_set_size ( FoobarPanelConfiguration* self, - gint value ); -void foobar_panel_configuration_set_spacing ( FoobarPanelConfiguration* self, - gint value ); -void foobar_panel_configuration_set_multi_monitor( FoobarPanelConfiguration* self, - gboolean value ); -void foobar_panel_configuration_set_items ( FoobarPanelConfiguration* self, - FoobarPanelItemConfiguration const* const* value, - gsize value_count ); - -typedef struct _FoobarLauncherConfiguration FoobarLauncherConfiguration; - -GType foobar_launcher_configuration_get_type ( void ); -FoobarLauncherConfiguration* foobar_launcher_configuration_new ( void ); -FoobarLauncherConfiguration* foobar_launcher_configuration_copy ( FoobarLauncherConfiguration const* self ); -void foobar_launcher_configuration_free ( FoobarLauncherConfiguration* self ); -gboolean foobar_launcher_configuration_equal ( FoobarLauncherConfiguration const* a, - FoobarLauncherConfiguration const* b ); -gint foobar_launcher_configuration_get_width ( FoobarLauncherConfiguration const* self ); -gint foobar_launcher_configuration_get_position ( FoobarLauncherConfiguration const* self ); -gint foobar_launcher_configuration_get_max_height( FoobarLauncherConfiguration const* self ); -void foobar_launcher_configuration_set_width ( FoobarLauncherConfiguration* self, - gint value ); -void foobar_launcher_configuration_set_position ( FoobarLauncherConfiguration* self, - gint value ); -void foobar_launcher_configuration_set_max_height( FoobarLauncherConfiguration* self, - gint value ); - -typedef struct _FoobarControlCenterConfiguration FoobarControlCenterConfiguration; - -GType foobar_control_center_configuration_get_type ( void ); -FoobarControlCenterConfiguration* foobar_control_center_configuration_new ( void ); -FoobarControlCenterConfiguration* foobar_control_center_configuration_copy ( FoobarControlCenterConfiguration const* self ); -void foobar_control_center_configuration_free ( FoobarControlCenterConfiguration* self ); -gboolean foobar_control_center_configuration_equal ( FoobarControlCenterConfiguration const* a, - FoobarControlCenterConfiguration const* b ); -gint foobar_control_center_configuration_get_width ( FoobarControlCenterConfiguration const* self ); -gint foobar_control_center_configuration_get_height ( FoobarControlCenterConfiguration const* self ); -FoobarScreenEdge foobar_control_center_configuration_get_position ( FoobarControlCenterConfiguration const* self ); -gint foobar_control_center_configuration_get_offset ( FoobarControlCenterConfiguration const* self ); -gint foobar_control_center_configuration_get_padding ( FoobarControlCenterConfiguration const* self ); -gint foobar_control_center_configuration_get_spacing ( FoobarControlCenterConfiguration const* self ); -FoobarOrientation foobar_control_center_configuration_get_orientation( FoobarControlCenterConfiguration const* self ); -FoobarControlCenterAlignment foobar_control_center_configuration_get_alignment ( FoobarControlCenterConfiguration const* self ); -FoobarControlCenterRow const* foobar_control_center_configuration_get_rows ( FoobarControlCenterConfiguration const* self, - gsize* out_count ); -void foobar_control_center_configuration_set_width ( FoobarControlCenterConfiguration* self, - gint value ); -void foobar_control_center_configuration_set_height ( FoobarControlCenterConfiguration* self, - gint value ); -void foobar_control_center_configuration_set_position ( FoobarControlCenterConfiguration* self, - FoobarScreenEdge value ); -void foobar_control_center_configuration_set_offset ( FoobarControlCenterConfiguration* self, - gint value ); -void foobar_control_center_configuration_set_padding ( FoobarControlCenterConfiguration* self, - gint value ); -void foobar_control_center_configuration_set_spacing ( FoobarControlCenterConfiguration* self, - gint value ); -void foobar_control_center_configuration_set_orientation( FoobarControlCenterConfiguration* self, - FoobarOrientation value ); -void foobar_control_center_configuration_set_rows ( FoobarControlCenterConfiguration* self, - FoobarControlCenterRow const* value, - gsize value_count ); -void foobar_control_center_configuration_set_alignment ( FoobarControlCenterConfiguration* self, - FoobarControlCenterAlignment value ); - -typedef struct _FoobarNotificationConfiguration FoobarNotificationConfiguration; - -GType foobar_notification_configuration_get_type ( void ); -FoobarNotificationConfiguration* foobar_notification_configuration_new ( void ); -FoobarNotificationConfiguration* foobar_notification_configuration_copy ( FoobarNotificationConfiguration const* self ); -void foobar_notification_configuration_free ( FoobarNotificationConfiguration* self ); -gboolean foobar_notification_configuration_equal ( FoobarNotificationConfiguration const* a, - FoobarNotificationConfiguration const* b ); -gint foobar_notification_configuration_get_width ( FoobarNotificationConfiguration const* self ); -gint foobar_notification_configuration_get_min_height ( FoobarNotificationConfiguration const* self ); -gint foobar_notification_configuration_get_spacing ( FoobarNotificationConfiguration const* self ); -gint foobar_notification_configuration_get_close_button_inset( FoobarNotificationConfiguration const* self ); -gchar const* foobar_notification_configuration_get_time_format ( FoobarNotificationConfiguration const* self ); -void foobar_notification_configuration_set_width ( FoobarNotificationConfiguration* self, - gint value ); -void foobar_notification_configuration_set_min_height ( FoobarNotificationConfiguration* self, - gint value ); -void foobar_notification_configuration_set_spacing ( FoobarNotificationConfiguration* self, - gint value ); -void foobar_notification_configuration_set_close_button_inset( FoobarNotificationConfiguration* self, - gint value ); -void foobar_notification_configuration_set_time_format ( FoobarNotificationConfiguration* self, - gchar const* value ); - -typedef struct _FoobarConfiguration FoobarConfiguration; - -GType foobar_configuration_get_type ( void ); -FoobarConfiguration* foobar_configuration_new ( void ); -FoobarConfiguration* foobar_configuration_copy ( FoobarConfiguration const* self ); -void foobar_configuration_free ( FoobarConfiguration* self ); -gboolean foobar_configuration_equal ( FoobarConfiguration const* a, - FoobarConfiguration const* b ); -FoobarGeneralConfiguration const* foobar_configuration_get_general ( FoobarConfiguration const* self ); -FoobarGeneralConfiguration* foobar_configuration_get_general_mut ( FoobarConfiguration* self ); -FoobarPanelConfiguration const* foobar_configuration_get_panel ( FoobarConfiguration const* self ); -FoobarPanelConfiguration* foobar_configuration_get_panel_mut ( FoobarConfiguration* self ); -FoobarLauncherConfiguration const* foobar_configuration_get_launcher ( FoobarConfiguration const* self ); -FoobarLauncherConfiguration* foobar_configuration_get_launcher_mut ( FoobarConfiguration* self ); -FoobarControlCenterConfiguration const* foobar_configuration_get_control_center ( FoobarConfiguration const* self ); -FoobarControlCenterConfiguration* foobar_configuration_get_control_center_mut( FoobarConfiguration* self ); -FoobarNotificationConfiguration const* foobar_configuration_get_notifications ( FoobarConfiguration const* self ); -FoobarNotificationConfiguration* foobar_configuration_get_notifications_mut ( FoobarConfiguration* self ); - - -G_DECLARE_FINAL_TYPE( FoobarConfigurationService, foobar_configuration_service, FOOBAR, CONFIGURATION_SERVICE, GObject ) - -FoobarConfigurationService* foobar_configuration_service_new ( void ); -FoobarConfiguration const* foobar_configuration_service_get_current( FoobarConfigurationService* self ); - -G_END_DECLS diff --git a/src/services/meson.build b/src/services/meson.build deleted file mode 100644 index 368c2f1..0000000 --- a/src/services/meson.build +++ /dev/null @@ -1,15 +0,0 @@ -foobar_sources += files( - 'battery-service.c', - 'clock-service.c', - 'brightness-service.c', - 'workspace-service.c', - 'notification-service.c', - 'audio-service.c', - 'network-service.c', - 'bluetooth-service.c', - 'application-service.c', - 'quick-answer-service.c', - 'configuration-service.c', -) - -subdir('quick-answers') diff --git a/src/services/network-service.c b/src/services/network-service.c deleted file mode 100644 index b2d1334..0000000 --- a/src/services/network-service.c +++ /dev/null @@ -1,1577 +0,0 @@ -#include "services/network-service.h" -#include -#include -#include - -typedef struct _ApNetworkMembership ApNetworkMembership; - -#define SCAN_REFRESH_INTERVAL 10000 - -// -// FoobarNetwork: -// -// Represents a Wi-Fi network, identified by its SSID. Instances contain a set of NMAccessPoints and the strength is -// updated to match that of the best available AP. -// - -struct _FoobarNetwork -{ - GObject parent_instance; - GBytes* ssid; - gchar* name; - gint strength; - gboolean is_active; - GHashTable* aps; -}; - -enum -{ - NETWORK_PROP_NAME = 1, - NETWORK_PROP_STRENGTH, - NETWORK_PROP_IS_ACTIVE, - N_NETWORK_PROPS, -}; - -static GParamSpec* network_props[N_NETWORK_PROPS] = { 0 }; - -static void foobar_network_class_init ( FoobarNetworkClass* klass ); -static void foobar_network_init ( FoobarNetwork* self ); -static void foobar_network_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_network_finalize ( GObject* object ); -static FoobarNetwork* foobar_network_new ( void ); -static void foobar_network_set_ssid ( FoobarNetwork* self, - GBytes* value ); -static void foobar_network_set_active ( FoobarNetwork* self, - gboolean value ); -static void foobar_network_add_ap ( FoobarNetwork* self, - NMAccessPoint* ap ); -static void foobar_network_remove_ap ( FoobarNetwork* self, - NMAccessPoint* ap ); -static void foobar_network_update_strength ( FoobarNetwork* self ); -static void foobar_network_handle_strength_change( GObject* ap, - GParamSpec* pspec, - gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarNetwork, foobar_network, G_TYPE_OBJECT ) - -// -// FoobarNetworkAdapterState: -// -// The current connection/connectivity state of a network adapter. -// - -G_DEFINE_ENUM_TYPE( - FoobarNetworkAdapterState, - foobar_network_adapter_state, - G_DEFINE_ENUM_VALUE( FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED, "disconnected" ), - G_DEFINE_ENUM_VALUE( FOOBAR_NETWORK_ADAPTER_STATE_CONNECTING, "connecting" ), - G_DEFINE_ENUM_VALUE( FOOBAR_NETWORK_ADAPTER_STATE_CONNECTED, "connected" ), - G_DEFINE_ENUM_VALUE( FOOBAR_NETWORK_ADAPTER_STATE_LIMITED, "limited" ) ) - -// -// FoobarNetworkAdapter: -// -// Base class for Wi-Fi/ethernet network adapters. -// - -typedef struct _FoobarNetworkAdapterPrivate -{ - NMDevice* device; - NMActiveConnection* connection; - FoobarNetworkAdapterState state; - gulong connectivity_handler_id; - gulong connection_handler_id; - gulong connection_state_handler_id; -} FoobarNetworkAdapterPrivate; - -enum -{ - ADAPTER_PROP_STATE = 1, - N_ADAPTER_PROPS, -}; - -static GParamSpec* adapter_props[N_ADAPTER_PROPS] = { 0 }; - -static void foobar_network_adapter_class_init ( FoobarNetworkAdapterClass* klass ); -static void foobar_network_adapter_init ( FoobarNetworkAdapter* self ); -static void foobar_network_adapter_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_network_adapter_finalize ( GObject* object ); -static void foobar_network_adapter_set_device ( FoobarNetworkAdapter* self, - NMDevice* value ); -static void foobar_network_adapter_update_state ( FoobarNetworkAdapter* self ); -static void foobar_network_adapter_handle_state_change ( GObject* sender, - GParamSpec* pspec, - gpointer userdata ); -static void foobar_network_adapter_handle_connection_change( GObject* sender, - GParamSpec* pspec, - gpointer userdata ); - -G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE( FoobarNetworkAdapter, foobar_network_adapter, G_TYPE_OBJECT ) - -// -// FoobarNetworkAdapterWired: -// -// Represents an ethernet network adapter. -// - -struct _FoobarNetworkAdapterWired -{ - FoobarNetworkAdapter parent_instance; - NMDeviceEthernet* device; - gulong speed_handler_id; -}; - -static void foobar_network_adapter_wired_class_init ( FoobarNetworkAdapterWiredClass* klass ); -static void foobar_network_adapter_wired_init ( FoobarNetworkAdapterWired* self ); -static void foobar_network_adapter_wired_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_network_adapter_wired_finalize ( GObject* object ); -static FoobarNetworkAdapterWired* foobar_network_adapter_wired_new ( NMDeviceEthernet* device ); -static void foobar_network_adapter_wired_handle_speed_change( GObject* device, - GParamSpec* pspec, - gpointer userdata ); - -enum -{ - WIRED_PROP_SPEED = 1, - N_WIRED_PROPS, -}; - -static GParamSpec* wired_props[N_WIRED_PROPS] = { 0 }; - -G_DEFINE_FINAL_TYPE( FoobarNetworkAdapterWired, foobar_network_adapter_wired, FOOBAR_TYPE_NETWORK_ADAPTER ) - -// -// FoobarNetworkAdapterWifi: -// -// Represents a Wi-Fi network adapter. -// - -struct _FoobarNetworkAdapterWifi -{ - FoobarNetworkAdapter parent_instance; - GHashTable* ap_networks; - GHashTable* named_networks; - GListStore* networks; - GtkFilterListModel* filtered_networks; - GtkSortListModel* sorted_networks; - FoobarNetwork* active; - gboolean is_scanning; - NMClient* client; - NMDeviceWifi* device; - gulong enabled_handler_id; - gulong active_handler_id; - gulong added_handler_id; - gulong removed_handler_id; - guint refresh_source_id; -}; - -enum -{ - WIFI_PROP_NETWORKS = 1, - WIFI_PROP_ACTIVE, - WIFI_PROP_IS_ENABLED, - WIFI_PROP_IS_SCANNING, - N_WIFI_PROPS, -}; - -static GParamSpec* wifi_props[N_WIFI_PROPS] = { 0 }; - -static void foobar_network_adapter_wifi_class_init ( FoobarNetworkAdapterWifiClass* klass ); -static void foobar_network_adapter_wifi_init ( FoobarNetworkAdapterWifi* self ); -static void foobar_network_adapter_wifi_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_network_adapter_wifi_set_property ( GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ); -static void foobar_network_adapter_wifi_finalize ( GObject* object ); -static FoobarNetworkAdapterWifi* foobar_network_adapter_wifi_new ( NMClient* client, - NMDeviceWifi* device ); -static void foobar_network_adapter_wifi_add_ap_to_network ( FoobarNetworkAdapterWifi* self, - NMAccessPoint* ap ); -static void foobar_network_adapter_wifi_update_active ( FoobarNetworkAdapterWifi* self ); -static void foobar_network_adapter_wifi_handle_ssid_change ( GObject* ap, - GParamSpec* pspec, - gpointer userdata ); -static void foobar_network_adapter_wifi_handle_enabled_change( GObject* client, - GParamSpec* pspec, - gpointer userdata ); -static void foobar_network_adapter_wifi_handle_active_change ( GObject* device, - GParamSpec* pspec, - gpointer userdata ); -static void foobar_network_adapter_wifi_handle_added ( NMDeviceWifi* device, - NMAccessPoint* ap, - gpointer userdata ); -static void foobar_network_adapter_wifi_handle_removed ( NMDeviceWifi* device, - NMAccessPoint* ap, - gpointer userdata ); -static gboolean foobar_network_adapter_wifi_refresh_list ( gpointer userdata ); -static gboolean foobar_network_adapter_wifi_filter_func ( gpointer item, - gpointer userdata ); -static gint foobar_network_adapter_wifi_sort_func ( gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarNetworkAdapterWifi, foobar_network_adapter_wifi, FOOBAR_TYPE_NETWORK_ADAPTER ) - -// -// ApNetworkMembership: -// -// Structure representing an AP's membership in a network. The AP is removed when the structure is destroyed. -// - -struct _ApNetworkMembership -{ - NMAccessPoint* ap; - FoobarNetwork* network; -}; - -static ApNetworkMembership* ap_network_membership_new ( NMAccessPoint* ap, - FoobarNetwork* network ); -static void ap_network_membership_destroy( ApNetworkMembership* self ); - -// -// FoobarNetworkService: -// -// Service monitoring the network state of the computer. This is implemented using the NetworkManager client library. -// - -struct _FoobarNetworkService -{ - GObject parent_instance; - NMClient* client; - FoobarNetworkAdapterWired* wired; - FoobarNetworkAdapterWifi* wifi; - FoobarNetworkAdapter* active; - gulong primary_handler_id; - gulong activating_handler_id; -}; - -enum -{ - PROP_WIRED = 1, - PROP_WIFI, - PROP_ACTIVE, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_network_service_class_init ( FoobarNetworkServiceClass* klass ); -static void foobar_network_service_init ( FoobarNetworkService* self ); -static void foobar_network_service_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_network_service_finalize ( GObject* object ); -static void foobar_network_service_handle_connection_change( GObject* client, - GParamSpec* pspec, - gpointer userdata ); -static void foobar_network_service_update_active ( FoobarNetworkService* self ); - -G_DEFINE_FINAL_TYPE( FoobarNetworkService, foobar_network_service, G_TYPE_OBJECT ) - -// --------------------------------------------------------------------------------------------------------------------- -// Wi-Fi Network -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for networks. -// -void foobar_network_class_init( FoobarNetworkClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_network_get_property; - object_klass->finalize = foobar_network_finalize; - - network_props[NETWORK_PROP_NAME] = g_param_spec_string( - "name", - "Name", - "The advertised SSID of the network.", - NULL, - G_PARAM_READABLE ); - network_props[NETWORK_PROP_STRENGTH] = g_param_spec_int( - "strength", - "Strength", - "The strength of the network in percent.", - 0, - 100, - 0, - G_PARAM_READABLE ); - network_props[NETWORK_PROP_IS_ACTIVE] = g_param_spec_boolean( - "is-active", - "Is Active", - "Indicates whether the network is currently used by the client.", - FALSE, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_NETWORK_PROPS, network_props ); -} - -// -// Instance initialization for networks. -// -void foobar_network_init( FoobarNetwork* self ) -{ - self->aps = g_hash_table_new_full( g_direct_hash, g_direct_equal, NULL, NULL ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_network_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarNetwork* self = (FoobarNetwork*)object; - - switch ( prop_id ) - { - case NETWORK_PROP_NAME: - g_value_set_string( value, foobar_network_get_name( self ) ); - break; - case NETWORK_PROP_STRENGTH: - g_value_set_int( value, foobar_network_get_strength( self ) ); - break; - case NETWORK_PROP_IS_ACTIVE: - g_value_set_boolean( value, foobar_network_is_active( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for networks. -// -void foobar_network_finalize( GObject* object ) -{ - FoobarNetwork* self = (FoobarNetwork*)object; - - GHashTableIter iter; - gpointer ap; - g_hash_table_iter_init( &iter, self->aps ); - while ( g_hash_table_iter_next( &iter, &ap, NULL ) ) - { - g_signal_handlers_disconnect_by_data( ap, self ); - } - - g_clear_pointer( &self->ssid, g_bytes_unref ); - g_clear_pointer( &self->name, g_free ); - g_clear_pointer( &self->aps, g_hash_table_unref ); - - G_OBJECT_CLASS( foobar_network_parent_class )->finalize( object ); -} - -// -// Create a new network instance. -// -FoobarNetwork* foobar_network_new( void ) -{ - return g_object_new( FOOBAR_TYPE_NETWORK, NULL ); -} - -// -// Get the advertized SSID of the network. -// -gchar const* foobar_network_get_name( FoobarNetwork* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NETWORK( self ), NULL ); - return self->name; -} - -// -// Get the current strength percentage value for the network. -// -gint foobar_network_get_strength( FoobarNetwork* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NETWORK( self ), 0 ); - return self->strength; -} - -// -// Check whether the network is currently used by the client. -// -gboolean foobar_network_is_active( FoobarNetwork* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NETWORK( self ), FALSE ); - return self->is_active; -} - -// -// Update the advertized SSID of the network. -// -// This also updates the string representation (which may be a lossy conversion). -// -void foobar_network_set_ssid( - FoobarNetwork* self, - GBytes* value ) -{ - g_return_if_fail( FOOBAR_IS_NETWORK( self ) ); - - if ( self->ssid != value ) - { - g_clear_pointer( &self->ssid, g_bytes_unref ); - g_clear_pointer( &self->name, g_free ); - - if ( value ) - { - self->ssid = g_bytes_ref( value ); - gsize ssid_len; - guint8 const* ssid_data = g_bytes_get_data( self->ssid, &ssid_len ); - self->name = nm_utils_ssid_to_utf8( ssid_data, ssid_len ); - } - - g_object_notify_by_pspec( G_OBJECT( self ), network_props[NETWORK_PROP_NAME] ); - } -} - -// -// Update the "active" flag of the network. This does not actually activate the network. -// -void foobar_network_set_active( - FoobarNetwork* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_NETWORK( self ) ); - - value = !!value; - if ( self->is_active != value ) - { - self->is_active = value; - g_object_notify_by_pspec( G_OBJECT( self ), network_props[NETWORK_PROP_NAME] ); - } -} - -// -// Include the specified AP when computing the network's strength. -// -void foobar_network_add_ap( - FoobarNetwork* self, - NMAccessPoint* ap ) -{ - if ( g_hash_table_add( self->aps, g_object_ref( ap ) ) ) - { - g_signal_connect( ap, "notify::strength", G_CALLBACK( foobar_network_handle_strength_change ), self ); - foobar_network_update_strength( self ); - } -} - -// -// Stop including the specified AP when computing the network's strength. -// -void foobar_network_remove_ap( - FoobarNetwork* self, - NMAccessPoint* ap ) -{ - if ( g_hash_table_remove( self->aps, ap ) ) - { - g_signal_handlers_disconnect_by_data( ap, self ); - foobar_network_update_strength( self ); - } -} - -// -// Update the network's "strength" property to match that of the best AP for it. -// -void foobar_network_update_strength( FoobarNetwork* self ) -{ - gint strength = 0; - GHashTableIter iter; - gpointer ap; - g_hash_table_iter_init( &iter, self->aps ); - while ( g_hash_table_iter_next( &iter, &ap, NULL ) ) - { - gint ap_strength = nm_access_point_get_strength( ap ); - strength = MAX( strength, ap_strength ); - } - - strength = CLAMP( strength, 0, 100 ); - if ( self->strength != strength ) - { - self->strength = strength; - g_object_notify_by_pspec( G_OBJECT( self ), network_props[NETWORK_PROP_STRENGTH] ); - } -} - -// -// Called when the strength of an access point for this network has changed. -// -void foobar_network_handle_strength_change( - GObject* ap, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)ap; - (void)pspec; - FoobarNetwork* self = (FoobarNetwork*)userdata; - - foobar_network_update_strength( self ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Network Adapter -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for network adapters. -// -void foobar_network_adapter_class_init( FoobarNetworkAdapterClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_network_adapter_get_property; - object_klass->finalize = foobar_network_adapter_finalize; - - adapter_props[ADAPTER_PROP_STATE] = g_param_spec_enum( - "state", - "State", - "The current connection state of the network.", - FOOBAR_TYPE_NETWORK_ADAPTER_STATE, - FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_ADAPTER_PROPS, adapter_props ); -} - -// -// Instance initialization for network adapters. -// -void foobar_network_adapter_init( FoobarNetworkAdapter* self ) -{ - (void)self; -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_network_adapter_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarNetworkAdapter* self = (FoobarNetworkAdapter*)object; - - switch ( prop_id ) - { - case ADAPTER_PROP_STATE: - g_value_set_enum( value, foobar_network_adapter_get_state( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for network adapters. -// -void foobar_network_adapter_finalize( GObject* object ) -{ - FoobarNetworkAdapter* self = (FoobarNetworkAdapter*)object; - - foobar_network_adapter_set_device( self, NULL ); - - G_OBJECT_CLASS( foobar_network_adapter_parent_class )->finalize( object ); -} - -// -// Get the current connection/connectivity state of the adapter. -// -FoobarNetworkAdapterState foobar_network_adapter_get_state( FoobarNetworkAdapter* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER( self ), FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED ); - - FoobarNetworkAdapterPrivate* priv = foobar_network_adapter_get_instance_private( self ); - return priv->state; -} - -// -// Update the backing network device which is used to derive the adapter's state. -// -void foobar_network_adapter_set_device( - FoobarNetworkAdapter* self, - NMDevice* value ) -{ - FoobarNetworkAdapterPrivate* priv = foobar_network_adapter_get_instance_private( self ); - if ( priv->device != value ) - { - g_clear_signal_handler( &priv->connection_state_handler_id, priv->connection ); - g_clear_signal_handler( &priv->connectivity_handler_id, priv->device ); - g_clear_signal_handler( &priv->connection_handler_id, priv->device ); - g_clear_object( &priv->connection ); - g_clear_object( &priv->device ); - - priv->device = value; - if ( priv->device ) - { - g_object_ref( priv->device ); - priv->connectivity_handler_id = g_signal_connect( - priv->device, - "notify::ip4-connectivity", - G_CALLBACK( foobar_network_adapter_handle_state_change ), self ); - priv->connection_handler_id = g_signal_connect( - priv->device, - "notify::active-connection", - G_CALLBACK( foobar_network_adapter_handle_connection_change ), self ); - priv->connection = nm_device_get_active_connection( priv->device ); - if ( priv->connection ) - { - g_object_ref( priv->connection ); - priv->connection_state_handler_id = g_signal_connect( - priv->connection, - "notify::state", - G_CALLBACK( foobar_network_adapter_handle_state_change ), - self ); - } - } - - foobar_network_adapter_update_state( self ); - } -} - -// -// Derive the connection/connectivity state for the network from the backing device. -// -void foobar_network_adapter_update_state( FoobarNetworkAdapter* self ) -{ - FoobarNetworkAdapterPrivate* priv = foobar_network_adapter_get_instance_private( self ); - - FoobarNetworkAdapterState state = FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED; - if ( priv->device ) - { - NMActiveConnection* connection = nm_device_get_active_connection( priv->device ); - if ( connection ) - { - switch ( nm_active_connection_get_state( connection ) ) - { - case NM_ACTIVE_CONNECTION_STATE_ACTIVATED: - state = ( nm_device_get_connectivity( priv->device, AF_INET ) == NM_CONNECTIVITY_FULL ) - ? FOOBAR_NETWORK_ADAPTER_STATE_CONNECTED - : FOOBAR_NETWORK_ADAPTER_STATE_LIMITED; - break; - case NM_ACTIVE_CONNECTION_STATE_ACTIVATING: - state = FOOBAR_NETWORK_ADAPTER_STATE_CONNECTING; - break; - case NM_ACTIVE_CONNECTION_STATE_DEACTIVATING: - case NM_ACTIVE_CONNECTION_STATE_DEACTIVATED: - case NM_ACTIVE_CONNECTION_STATE_UNKNOWN: - default: - state = FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED; - break; - } - } - } - - if ( priv->state != state ) - { - priv->state = state; - g_object_notify_by_pspec( G_OBJECT( self ), adapter_props[ADAPTER_PROP_STATE] ); - } -} - -// -// Called when the "ip4-connectivity" property of the NetworkManager device or the "state" property of the active -// connection has changed. -// -static void foobar_network_adapter_handle_state_change( - GObject* sender, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)sender; - (void)pspec; - FoobarNetworkAdapter* self = (FoobarNetworkAdapter*)userdata; - - foobar_network_adapter_update_state( self ); -} - -// -// Called when the "active-connection" property of the NetworkManager device has changed. -// -// This updates the state of the adapter and sets the connection up for state change listening. -// -static void foobar_network_adapter_handle_connection_change( - GObject* sender, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)sender; - (void)pspec; - FoobarNetworkAdapter* self = (FoobarNetworkAdapter*)userdata; - FoobarNetworkAdapterPrivate* priv = foobar_network_adapter_get_instance_private( self ); - - g_clear_signal_handler( &priv->connection_state_handler_id, priv->connection ); - g_clear_object( &priv->connection ); - - priv->connection = nm_device_get_active_connection( priv->device ); - if ( priv->connection ) - { - g_object_ref( priv->connection ); - priv->connection_state_handler_id = g_signal_connect( - priv->connection, - "notify::state", - G_CALLBACK( foobar_network_adapter_handle_state_change ), - self ); - } - - foobar_network_adapter_update_state( self ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Wired Network Adapter -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for wired network adapters. -// -void foobar_network_adapter_wired_class_init( FoobarNetworkAdapterWiredClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_network_adapter_wired_get_property; - object_klass->finalize = foobar_network_adapter_wired_finalize; - - wired_props[WIRED_PROP_SPEED] = g_param_spec_int64( - "speed", - "Speed", - "The supported speed of the adapter in Mbit/s.", - 0, - INT64_MAX, - 0, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_WIRED_PROPS, wired_props ); -} - -// -// Instance initialization for wired network adapters. -// -void foobar_network_adapter_wired_init( FoobarNetworkAdapterWired* self ) -{ - (void)self; -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_network_adapter_wired_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarNetworkAdapterWired* self = (FoobarNetworkAdapterWired*)object; - - switch ( prop_id ) - { - case WIRED_PROP_SPEED: - g_value_set_int64( value, foobar_network_adapter_wired_get_speed( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for wired network adapters. -// -void foobar_network_adapter_wired_finalize( GObject* object ) -{ - FoobarNetworkAdapterWired* self = (FoobarNetworkAdapterWired*)object; - - g_clear_signal_handler( &self->speed_handler_id, self->device ); - g_clear_object( &self->device ); - - G_OBJECT_CLASS( foobar_network_adapter_wired_parent_class )->finalize( object ); -} - -// -// Create a new wired network adapter wrapping the provided NetworkManager device. -// -FoobarNetworkAdapterWired* foobar_network_adapter_wired_new( NMDeviceEthernet* device ) -{ - FoobarNetworkAdapterWired* self = g_object_new( FOOBAR_TYPE_NETWORK_ADAPTER_WIRED, NULL ); - foobar_network_adapter_set_device( FOOBAR_NETWORK_ADAPTER( self ), NM_DEVICE( device ) ); - - self->device = g_object_ref( device ); - self->speed_handler_id = g_signal_connect( - self->device, - "notify::speed", - G_CALLBACK( foobar_network_adapter_wired_handle_speed_change ), - self ); - return self; -} - -// -// Get the supported speed of the adapter in Mbit/s. -// -gint64 foobar_network_adapter_wired_get_speed( FoobarNetworkAdapterWired* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIRED( self ), 0 ); - return nm_device_ethernet_get_speed( self->device ); -} - -// -// Called when the "speed" property of the NetworkManager device changes. -// -void foobar_network_adapter_wired_handle_speed_change( - GObject* device, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)device; - (void)pspec; - FoobarNetworkAdapterWired* self = (FoobarNetworkAdapterWired*)userdata; - - g_object_notify_by_pspec( G_OBJECT( self ), wired_props[WIRED_PROP_SPEED] ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Wireless Network Adapter -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for wireless network adapters. -// -void foobar_network_adapter_wifi_class_init( FoobarNetworkAdapterWifiClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_network_adapter_wifi_get_property; - object_klass->set_property = foobar_network_adapter_wifi_set_property; - object_klass->finalize = foobar_network_adapter_wifi_finalize; - - wifi_props[WIFI_PROP_NETWORKS] = g_param_spec_object( - "networks", - "Networks", - "Sorted list of available networks.", - G_TYPE_LIST_MODEL, - G_PARAM_READABLE ); - wifi_props[WIFI_PROP_ACTIVE] = g_param_spec_object( - "active", - "Active", - "The currently active network (if any).", - FOOBAR_TYPE_NETWORK, - G_PARAM_READABLE ); - wifi_props[WIFI_PROP_IS_ENABLED] = g_param_spec_boolean( - "is-enabled", - "Is Enabled", - "Indicates whether the wifi adapter is currently enabled.", - FALSE, - G_PARAM_READWRITE ); - wifi_props[WIFI_PROP_IS_SCANNING] = g_param_spec_boolean( - "is-scanning", - "Is Scanning", - "Indicates whether the wifi adapter is currently scanning for available networks.", - FALSE, - G_PARAM_READWRITE ); - g_object_class_install_properties( object_klass, N_WIFI_PROPS, wifi_props ); -} - -// -// Instance initialization for wireless network adapters. -// -void foobar_network_adapter_wifi_init( FoobarNetworkAdapterWifi* self ) -{ - self->ap_networks = g_hash_table_new_full( - g_direct_hash, - g_direct_equal, - g_object_unref, - (GDestroyNotify)ap_network_membership_destroy ); - self->named_networks = g_hash_table_new_full( - g_bytes_hash, - g_bytes_equal, - (GDestroyNotify)g_bytes_unref, - g_object_unref ); - self->networks = g_list_store_new( FOOBAR_TYPE_NETWORK ); - - GtkCustomFilter* filter = gtk_custom_filter_new( foobar_network_adapter_wifi_filter_func, NULL, NULL ); - self->filtered_networks = gtk_filter_list_model_new( - G_LIST_MODEL( g_object_ref( self->networks ) ), - GTK_FILTER( filter ) ); - - GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_network_adapter_wifi_sort_func, NULL, NULL ); - self->sorted_networks = gtk_sort_list_model_new( - G_LIST_MODEL( g_object_ref( self->filtered_networks ) ), - GTK_SORTER( sorter ) ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_network_adapter_wifi_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)object; - - switch ( prop_id ) - { - case WIFI_PROP_NETWORKS: - g_value_set_object( value, foobar_network_adapter_wifi_get_networks( self ) ); - break; - case WIFI_PROP_ACTIVE: - g_value_set_object( value, foobar_network_adapter_wifi_get_active( self ) ); - break; - case WIFI_PROP_IS_ENABLED: - g_value_set_boolean( value, foobar_network_adapter_wifi_is_enabled( self ) ); - break; - case WIFI_PROP_IS_SCANNING: - g_value_set_boolean( value, foobar_network_adapter_wifi_is_scanning( 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_network_adapter_wifi_set_property( - GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ) -{ - FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)object; - - switch ( prop_id ) - { - case WIFI_PROP_IS_ENABLED: - foobar_network_adapter_wifi_set_enabled( self, g_value_get_boolean( value ) ); - break; - case WIFI_PROP_IS_SCANNING: - foobar_network_adapter_wifi_set_scanning( self, g_value_get_boolean( value ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for wireless network adapters. -// -void foobar_network_adapter_wifi_finalize( GObject* object ) -{ - FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)object; - - GPtrArray const* aps = nm_device_wifi_get_access_points( self->device ); - for ( guint i = 0; i < aps->len; ++i ) - { - NMAccessPoint* ap = g_ptr_array_index( aps, i ); - g_signal_handlers_disconnect_by_data( ap, self ); - } - - g_clear_handle_id( &self->refresh_source_id, g_source_remove ); - g_clear_signal_handler( &self->enabled_handler_id, self->client ); - g_clear_signal_handler( &self->active_handler_id, self->device ); - g_clear_signal_handler( &self->added_handler_id, self->device ); - g_clear_signal_handler( &self->removed_handler_id, self->device ); - g_clear_object( &self->client ); - g_clear_object( &self->device ); - g_clear_object( &self->sorted_networks ); - g_clear_object( &self->filtered_networks ); - g_clear_object( &self->networks ); - g_clear_object( &self->active ); - g_clear_pointer( &self->named_networks, g_hash_table_unref ); - g_clear_pointer( &self->ap_networks, g_hash_table_unref ); - - G_OBJECT_CLASS( foobar_network_adapter_wifi_parent_class )->finalize( object ); -} - -// -// Create a new wireless network adapter wrapping the provided NetworkManager device. -// -FoobarNetworkAdapterWifi* foobar_network_adapter_wifi_new( - NMClient* client, - NMDeviceWifi* device ) -{ - FoobarNetworkAdapterWifi* self = g_object_new( FOOBAR_TYPE_NETWORK_ADAPTER_WIFI, NULL ); - foobar_network_adapter_set_device( FOOBAR_NETWORK_ADAPTER( self ), NM_DEVICE( device ) ); - - self->client = g_object_ref( client ); - self->device = g_object_ref( device ); - self->enabled_handler_id = g_signal_connect( - self->client, - "notify::wireless-enabled", - G_CALLBACK( foobar_network_adapter_wifi_handle_enabled_change ), - self ); - self->active_handler_id = g_signal_connect( - self->device, - "notify::active-access-point", - G_CALLBACK( foobar_network_adapter_wifi_handle_active_change ), - self ); - self->added_handler_id = g_signal_connect( - self->device, - "access-point-added", - G_CALLBACK( foobar_network_adapter_wifi_handle_added ), - self ); - self->removed_handler_id = g_signal_connect( - self->device, - "access-point-removed", - G_CALLBACK( foobar_network_adapter_wifi_handle_removed ), - self ); - - GPtrArray const* aps = nm_device_wifi_get_access_points( self->device ); - for ( guint i = 0; i < aps->len; ++i ) - { - NMAccessPoint* ap = g_ptr_array_index( aps, i ); - g_signal_connect( - ap, - "notify::ssid", - G_CALLBACK( foobar_network_adapter_wifi_handle_ssid_change ), - self ); - foobar_network_adapter_wifi_add_ap_to_network( self, ap ); - } - - foobar_network_adapter_wifi_update_active( self ); - return self; -} - -// -// Get a sorted list of available networks. -// -GListModel* foobar_network_adapter_wifi_get_networks( FoobarNetworkAdapterWifi* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ), NULL ); - return G_LIST_MODEL( self->sorted_networks ); -} - -// -// Get the currently active network (if any). -// -FoobarNetwork* foobar_network_adapter_wifi_get_active( FoobarNetworkAdapterWifi* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ), NULL ); - return self->active; -} - -// -// Check whether the wifi adapter is currently enabled. -// -gboolean foobar_network_adapter_wifi_is_enabled( FoobarNetworkAdapterWifi* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ), FALSE ); - return nm_client_wireless_get_enabled( self->client ); -} - -// -// Check whether the wifi adapter is currently scanning for available networks. -// -gboolean foobar_network_adapter_wifi_is_scanning( FoobarNetworkAdapterWifi* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ), FALSE ); - return self->is_scanning; -} - -// -// Update whether the wifi adapter is currently enabled. -// -void foobar_network_adapter_wifi_set_enabled( - FoobarNetworkAdapterWifi* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ) ); - - if ( !value ) { foobar_network_adapter_wifi_set_scanning( self, FALSE ); } - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - nm_client_wireless_set_enabled( self->client, value ); -#pragma GCC diagnostic pop -} - -// -// Update whether the wifi adapter is currently scanning for available networks. -// -void foobar_network_adapter_wifi_set_scanning( - FoobarNetworkAdapterWifi* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_NETWORK_ADAPTER_WIFI( self ) ); - - value = value && foobar_network_adapter_wifi_is_enabled( self ); - if ( self->is_scanning != value ) - { - self->is_scanning = value; - g_object_notify_by_pspec( G_OBJECT( self ), wifi_props[WIFI_PROP_IS_SCANNING] ); - - // Periodically start scans until "is-scanning" is disabled again. - - g_clear_handle_id( &self->refresh_source_id, g_source_remove ); - if ( self->is_scanning ) - { - foobar_network_adapter_wifi_refresh_list( self ); - self->refresh_source_id = g_timeout_add( - SCAN_REFRESH_INTERVAL, - foobar_network_adapter_wifi_refresh_list, - self ); - } - } -} - -// -// Ensure that a network for the specified access point exists. -// -// If the AP has an SSID, it is added to the network for this SSID. Otherwise, a new network for this AP is created just -// for this network. -// -void foobar_network_adapter_wifi_add_ap_to_network( - FoobarNetworkAdapterWifi* self, - NMAccessPoint* ap ) -{ - // The AP is automatically added/removed from the network when using g_hash_table_replace because of the - // ApNetworkMembership destroy function. - - GBytes* ssid = nm_access_point_get_ssid( ap ); - if ( ssid ) - { - FoobarNetwork* network = g_hash_table_lookup( self->named_networks, ssid ); - if ( !network ) - { - network = foobar_network_new( ); - foobar_network_set_ssid( network, ssid ); - g_list_store_append( self->networks, network ); - g_hash_table_insert( self->named_networks, g_bytes_ref( ssid ), network ); - } - - g_hash_table_replace( self->ap_networks, g_object_ref( ap ), ap_network_membership_new( ap, network ) ); - } - else - { - g_autoptr( FoobarNetwork ) network = foobar_network_new( ); - g_hash_table_replace( self->ap_networks, g_object_ref( ap ), ap_network_membership_new( ap, network ) ); - } - - gtk_filter_changed( gtk_filter_list_model_get_filter( self->filtered_networks ), GTK_FILTER_CHANGE_DIFFERENT ); -} - -// -// Update the active network based on the active access point. -// -void foobar_network_adapter_wifi_update_active( FoobarNetworkAdapterWifi* self ) -{ - NMAccessPoint* active_ap = nm_device_wifi_get_active_access_point( self->device ); - ApNetworkMembership* membership = active_ap ? g_hash_table_lookup( self->ap_networks, active_ap ) : NULL; - FoobarNetwork* new_active = membership ? membership->network : NULL; - - if ( self->active != new_active ) - { - if ( self->active ) - { - foobar_network_set_active( self->active, FALSE ); - } - g_clear_object( &self->active ); - - self->active = new_active; - if ( self->active ) - { - g_object_ref( self->active ); - foobar_network_set_active( self->active, TRUE ); - } - - g_object_notify_by_pspec( G_OBJECT( self ), wifi_props[WIFI_PROP_ACTIVE] ); - } -} - -// -// Called when the SSID for an access point changes. -// -// This will remove the access point from the old network and associate it with its new network. -// -void foobar_network_adapter_wifi_handle_ssid_change( - GObject* ap, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)pspec; - FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata; - - foobar_network_adapter_wifi_add_ap_to_network( self, NM_ACCESS_POINT( ap ) ); - foobar_network_adapter_wifi_update_active( self ); -} - -// -// Called when the "wireless-enabled" state of the client changes. -// -void foobar_network_adapter_wifi_handle_enabled_change( - GObject* client, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)client; - (void)pspec; - FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata; - - g_object_notify_by_pspec( G_OBJECT( self ), wifi_props[WIFI_PROP_IS_ENABLED] ); -} - -// -// Called when the active access point of the backing device changes. -// -void foobar_network_adapter_wifi_handle_active_change( - GObject* device, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)device; - (void)pspec; - FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata; - - foobar_network_adapter_wifi_update_active( self ); -} - -// -// Called when a new access point is added. -// -// This will associate the access point with its network. -// -void foobar_network_adapter_wifi_handle_added( - NMDeviceWifi* device, - NMAccessPoint* ap, - gpointer userdata ) -{ - (void)device; - FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata; - - g_signal_connect( - ap, - "notify::ssid", - G_CALLBACK( foobar_network_adapter_wifi_handle_ssid_change ), - self ); - foobar_network_adapter_wifi_add_ap_to_network( self, ap ); -} - -// -// Called when an access point is removed. -// -// This will remove it from its network. -// -void foobar_network_adapter_wifi_handle_removed( - NMDeviceWifi* device, - NMAccessPoint* ap, - gpointer userdata ) -{ - (void)device; - FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata; - - g_signal_handlers_disconnect_by_data( ap, self ); - g_hash_table_remove( self->ap_networks, ap ); // Will remove the AP from the network. - gtk_filter_changed( gtk_filter_list_model_get_filter( self->filtered_networks ), GTK_FILTER_CHANGE_MORE_STRICT ); -} - -// -// Periodic callback to start a network scan. -// -gboolean foobar_network_adapter_wifi_refresh_list( gpointer userdata ) -{ - FoobarNetworkAdapterWifi* self = (FoobarNetworkAdapterWifi*)userdata; - - g_autoptr( GError ) error = NULL; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - if ( !nm_device_wifi_request_scan( self->device, NULL, &error ) ) -#pragma GCC diagnostic pop - { - g_warning( "Unable to start Wi-Fi network scan: %s", error->message ); - } - - return G_SOURCE_CONTINUE; -} - -// -// Filtering callback for the list of networks. Only networks with at least one AP are shown. -// -gboolean foobar_network_adapter_wifi_filter_func( - gpointer item, - gpointer userdata ) -{ - (void)userdata; - - FoobarNetwork* network = (FoobarNetwork*)item; - return g_hash_table_size( network->aps ) > 0; -} - -// -// Sorting callback for the list of networks. -// -gint foobar_network_adapter_wifi_sort_func( - gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ) -{ - (void)userdata; - - FoobarNetwork* network_a = (FoobarNetwork*)item_a; - FoobarNetwork* network_b = (FoobarNetwork*)item_b; - return g_strcmp0( foobar_network_get_name( network_a ), foobar_network_get_name( network_b ) ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Access Point Network Memberships -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create an association between an access point and the network it belongs to. -// -ApNetworkMembership* ap_network_membership_new( - NMAccessPoint* ap, - FoobarNetwork* network ) -{ - ApNetworkMembership* self = g_new0( ApNetworkMembership, 1 ); - self->ap = ap; - self->network = g_object_ref( network ); - foobar_network_add_ap( self->network, self->ap ); - return self; -} - -// -// Destroy an association between an access point and its previous network. -// -void ap_network_membership_destroy( ApNetworkMembership* self ) -{ - if ( self ) - { - foobar_network_remove_ap( self->network, self->ap ); - g_object_unref( self->network ); - g_free( self ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Service Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the network service. -// -void foobar_network_service_class_init( FoobarNetworkServiceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_network_service_get_property; - object_klass->finalize = foobar_network_service_finalize; - - props[PROP_WIRED] = g_param_spec_object( - "wired", - "Wired", - "The wired network adapter (if available).", - FOOBAR_TYPE_NETWORK_ADAPTER_WIRED, - G_PARAM_READABLE ); - props[PROP_WIFI] = g_param_spec_object( - "wifi", - "WiFi", - "The wireless network adapter (if available).", - FOOBAR_TYPE_NETWORK_ADAPTER_WIFI, - G_PARAM_READABLE ); - props[PROP_ACTIVE] = g_param_spec_object( - "active", - "Active", - "The network adapter currently in use.", - FOOBAR_TYPE_NETWORK_ADAPTER, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for the network service. -// -void foobar_network_service_init( FoobarNetworkService* self ) -{ - g_autoptr( GError ) error = NULL; - self->client = nm_client_new( NULL, &error ); - if ( !self->client ) - { - g_warning( "Unable to connect to NetworkManager: %s", error->message ); - return; - } - - NMDeviceEthernet* device_wired = NULL; - NMDeviceWifi* device_wifi = NULL; - GPtrArray const* devices = nm_client_get_devices( self->client ); - for ( guint i = 0; i < devices->len; ++i ) - { - // XXX: Add support for multiple devices - NMDevice* device = g_ptr_array_index( devices, i ); - if ( !device_wired && NM_IS_DEVICE_ETHERNET( device ) ) - { - device_wired = NM_DEVICE_ETHERNET( device ); - } - if ( !device_wifi && NM_IS_DEVICE_WIFI( device ) ) - { - device_wifi = NM_DEVICE_WIFI( device ); - } - if ( device_wired && device_wifi ) - { - break; - } - } - - self->wired = device_wired ? foobar_network_adapter_wired_new( device_wired ) : NULL; - self->wifi = device_wifi ? foobar_network_adapter_wifi_new( self->client, device_wifi ) : NULL; - - foobar_network_service_update_active( self ); - self->primary_handler_id = g_signal_connect( - self->client, - "notify::primary-connection", - G_CALLBACK( foobar_network_service_handle_connection_change ), - self ); - self->activating_handler_id = g_signal_connect( - self->client, - "notify::activating-connection", - G_CALLBACK( foobar_network_service_handle_connection_change ), - self ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_network_service_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarNetworkService* self = (FoobarNetworkService*)object; - - switch ( prop_id ) - { - case PROP_WIRED: - g_value_set_object( value, foobar_network_service_get_wired( self ) ); - break; - case PROP_WIFI: - g_value_set_object( value, foobar_network_service_get_wifi( self ) ); - break; - case PROP_ACTIVE: - g_value_set_object( value, foobar_network_service_get_active( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for the network service. -// -void foobar_network_service_finalize( GObject* object ) -{ - FoobarNetworkService* self = (FoobarNetworkService*)object; - - g_clear_signal_handler( &self->primary_handler_id, self->client ); - g_clear_signal_handler( &self->activating_handler_id, self->client ); - g_clear_object( &self->wifi ); - g_clear_object( &self->wired ); - g_clear_object( &self->client ); - - G_OBJECT_CLASS( foobar_network_service_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new network service instance. -// -FoobarNetworkService* foobar_network_service_new( void ) -{ - return g_object_new( FOOBAR_TYPE_NETWORK_SERVICE, NULL ); -} - -// -// Get the wired network adapter (if available). -// -FoobarNetworkAdapterWired* foobar_network_service_get_wired( FoobarNetworkService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NETWORK_SERVICE( self ), NULL ); - return self->wired; -} - -// -// Get the wireless network adapter (if available). -// -FoobarNetworkAdapterWifi* foobar_network_service_get_wifi( FoobarNetworkService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NETWORK_SERVICE( self ), NULL ); - return self->wifi; -} - -// -// Get the network adapter currently in use. -// -FoobarNetworkAdapter* foobar_network_service_get_active( FoobarNetworkService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NETWORK_SERVICE( self ), NULL ); - return self->active; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called when either the primary connection or the activating connection of the client has changed. -// -void foobar_network_service_handle_connection_change( - GObject* client, - GParamSpec* pspec, - gpointer userdata ) -{ - (void)client; - (void)pspec; - FoobarNetworkService* self = (FoobarNetworkService*)userdata; - - foobar_network_service_update_active( self ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Methods -// --------------------------------------------------------------------------------------------------------------------- - -// -// Update the active network adapter. -// -// This uses the primary connection of the client to determine whether the wireless or wired adapter is used. Otherwise, -// the activating connection is used instead. -// -void foobar_network_service_update_active( FoobarNetworkService* self ) -{ - NMActiveConnection* connection = nm_client_get_primary_connection( self->client ); - connection = connection ? connection : nm_client_get_activating_connection( self->client ); - gchar const* type = connection ? nm_active_connection_get_connection_type( connection ) : NULL; - - FoobarNetworkAdapter* new_active; - if ( !g_strcmp0( type, "802-11-wireless" ) ) - { - new_active = FOOBAR_NETWORK_ADAPTER( self->wifi ); - } - else if ( !g_strcmp0( type, "802-3-ethernet" ) ) - { - new_active = FOOBAR_NETWORK_ADAPTER( self->wired ); - } - else - { - new_active = self->wifi ? FOOBAR_NETWORK_ADAPTER( self->wifi ) : FOOBAR_NETWORK_ADAPTER( self->wired ); - } - - if ( self->active != new_active ) - { - self->active = new_active; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ACTIVE] ); - } -} diff --git a/src/services/network-service.h b/src/services/network-service.h deleted file mode 100644 index 2307cec..0000000 --- a/src/services/network-service.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_NETWORK foobar_network_get_type( ) -#define FOOBAR_TYPE_NETWORK_ADAPTER_STATE foobar_network_adapter_state_get_type( ) -#define FOOBAR_TYPE_NETWORK_ADAPTER foobar_network_adapter_get_type( ) -#define FOOBAR_TYPE_NETWORK_ADAPTER_WIRED foobar_network_adapter_wired_get_type( ) -#define FOOBAR_TYPE_NETWORK_ADAPTER_WIFI foobar_network_adapter_wifi_get_type( ) -#define FOOBAR_TYPE_NETWORK_SERVICE foobar_network_service_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarNetwork, foobar_network, FOOBAR, NETWORK, GObject ) - -gchar const* foobar_network_get_name ( FoobarNetwork* self ); -gint foobar_network_get_strength( FoobarNetwork* self ); -gboolean foobar_network_is_active ( FoobarNetwork* self ); - -typedef enum -{ - FOOBAR_NETWORK_ADAPTER_STATE_DISCONNECTED, - FOOBAR_NETWORK_ADAPTER_STATE_CONNECTING, - FOOBAR_NETWORK_ADAPTER_STATE_CONNECTED, - FOOBAR_NETWORK_ADAPTER_STATE_LIMITED, -} FoobarNetworkAdapterState; - -GType foobar_network_adapter_state_get_type( void ); - -G_DECLARE_DERIVABLE_TYPE( FoobarNetworkAdapter, foobar_network_adapter, FOOBAR, NETWORK_ADAPTER, GObject ) - -struct _FoobarNetworkAdapterClass -{ - GObjectClass parent_class; -}; - -FoobarNetworkAdapterState foobar_network_adapter_get_state( FoobarNetworkAdapter* self ); - -G_DECLARE_FINAL_TYPE( FoobarNetworkAdapterWired, foobar_network_adapter_wired, FOOBAR, NETWORK_ADAPTER_WIRED, FoobarNetworkAdapter ) - -gint64 foobar_network_adapter_wired_get_speed( FoobarNetworkAdapterWired* self ); - -G_DECLARE_FINAL_TYPE( FoobarNetworkAdapterWifi, foobar_network_adapter_wifi, FOOBAR, NETWORK_ADAPTER_WIFI, FoobarNetworkAdapter ) - -GListModel* foobar_network_adapter_wifi_get_networks( FoobarNetworkAdapterWifi* self ); -FoobarNetwork* foobar_network_adapter_wifi_get_active ( FoobarNetworkAdapterWifi* self ); -gboolean foobar_network_adapter_wifi_is_enabled ( FoobarNetworkAdapterWifi* self ); -gboolean foobar_network_adapter_wifi_is_scanning ( FoobarNetworkAdapterWifi* self ); -void foobar_network_adapter_wifi_set_enabled ( FoobarNetworkAdapterWifi* self, - gboolean value ); -void foobar_network_adapter_wifi_set_scanning( FoobarNetworkAdapterWifi* self, - gboolean value ); - -G_DECLARE_FINAL_TYPE( FoobarNetworkService, foobar_network_service, FOOBAR, NETWORK_SERVICE, GObject ) - -FoobarNetworkService* foobar_network_service_new ( void ); -FoobarNetworkAdapterWired* foobar_network_service_get_wired ( FoobarNetworkService* self ); -FoobarNetworkAdapterWifi* foobar_network_service_get_wifi ( FoobarNetworkService* self ); -FoobarNetworkAdapter* foobar_network_service_get_active( FoobarNetworkService* self ); - -G_END_DECLS diff --git a/src/services/notification-service.c b/src/services/notification-service.c deleted file mode 100644 index 1a085f0..0000000 --- a/src/services/notification-service.c +++ /dev/null @@ -1,1833 +0,0 @@ -#include "services/notification-service.h" -#include "dbus/notifications.h" -#include "utils.h" -#include -#include - -#define DEFAULT_TIMEOUT 3000 - -// -// FoobarNotificationUrgency: -// -// The urgency of a notification as provided by the application that sent it. -// - -G_DEFINE_ENUM_TYPE( - FoobarNotificationUrgency, - foobar_notification_urgency, - G_DEFINE_ENUM_VALUE( FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_COMFORT, "loss-of-comfort" ), - G_DEFINE_ENUM_VALUE( FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY, "loss-of-money" ), - G_DEFINE_ENUM_VALUE( FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_LIFE, "loss-of-life" ) ) - -// -// FoobarNotificationAction: -// -// A labeled action for a notification that can be invoked by the notification daemon (us). -// - -struct _FoobarNotificationAction -{ - GObject parent_instance; - FoobarNotification* notification; - gchar* id; - gchar* label; -}; - -enum -{ - ACTION_PROP_ID = 1, - ACTION_PROP_LABEL, - N_ACTION_PROPS, -}; - -static GParamSpec* action_props[N_ACTION_PROPS] = { 0 }; - -static void foobar_notification_action_class_init ( FoobarNotificationActionClass* klass ); -static void foobar_notification_action_init ( FoobarNotificationAction* self ); -static void foobar_notification_action_get_property( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_notification_action_finalize ( GObject* object ); -static FoobarNotificationAction* foobar_notification_action_new ( FoobarNotification* notification ); -static void foobar_notification_action_set_id ( FoobarNotificationAction* self, - gchar const* value ); -static void foobar_notification_action_set_label ( FoobarNotificationAction* self, - gchar const* value ); - -G_DEFINE_FINAL_TYPE( FoobarNotificationAction, foobar_notification_action, G_TYPE_OBJECT ) - -// -// FoobarNotification: -// -// A notification received by the notification daemon (us). -// - -struct _FoobarNotification -{ - GObject parent_instance; - guint timeout_id; - FoobarNotificationService* service; - guint id; - GPtrArray* actions; - gchar* app_entry; - gchar* app_name; - gchar* body; - gchar* summary; - GdkPixbuf* image; - gchar* image_path; - gchar* image_data; - gboolean is_dismissed; - gboolean is_resident; - gboolean is_transient; - GDateTime* time; - gint64 timeout; - FoobarNotificationUrgency urgency; -}; - -enum -{ - NOTIFICATION_PROP_ID = 1, - NOTIFICATION_PROP_APP_ENTRY, - NOTIFICATION_PROP_APP_NAME, - NOTIFICATION_PROP_BODY, - NOTIFICATION_PROP_SUMMARY, - NOTIFICATION_PROP_IMAGE, - NOTIFICATION_PROP_IS_DISMISSED, - NOTIFICATION_PROP_IS_RESIDENT, - NOTIFICATION_PROP_IS_TRANSIENT, - NOTIFICATION_PROP_TIME, - NOTIFICATION_PROP_TIMEOUT, - NOTIFICATION_PROP_URGENCY, - N_NOTIFICATION_PROPS, -}; - -static GParamSpec* notification_props[N_NOTIFICATION_PROPS] = { 0 }; - -static void foobar_notification_class_init ( FoobarNotificationClass* klass ); -static void foobar_notification_init ( FoobarNotification* self ); -static void foobar_notification_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_notification_finalize ( GObject* object ); -static FoobarNotification* foobar_notification_new ( FoobarNotificationService* service ); -static gchar const* foobar_notification_get_image_path ( FoobarNotification* self ); -static gchar const* foobar_notification_get_image_data ( FoobarNotification* self ); -static void foobar_notification_set_id ( FoobarNotification* self, - guint value ); -static void foobar_notification_set_app_entry ( FoobarNotification* self, - gchar const* value ); -static void foobar_notification_set_app_name ( FoobarNotification* self, - gchar const* value ); -static void foobar_notification_set_body ( FoobarNotification* self, - gchar const* value ); -static void foobar_notification_set_summary ( FoobarNotification* self, - gchar const* value ); -static void foobar_notification_set_image_from_path( FoobarNotification* self, - gchar const* value ); -static void foobar_notification_set_image_from_data( FoobarNotification* self, - gchar const* value ); -static void foobar_notification_set_image ( FoobarNotification* self, - GdkPixbuf* value ); -static void foobar_notification_set_dismissed ( FoobarNotification* self, - gboolean value ); -static void foobar_notification_set_resident ( FoobarNotification* self, - gboolean value ); -static void foobar_notification_set_transient ( FoobarNotification* self, - gboolean value ); -static void foobar_notification_set_time ( FoobarNotification* self, - GDateTime* value ); -static void foobar_notification_set_timeout ( FoobarNotification* self, - gint64 value ); -static void foobar_notification_set_urgency ( FoobarNotification* self, - FoobarNotificationUrgency value ); -static void foobar_notification_add_action ( FoobarNotification* self, - FoobarNotificationAction* action ); -static void foobar_notification_free_action ( gpointer action ); -static gboolean foobar_notification_handle_timeout ( gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarNotification, foobar_notification, G_TYPE_OBJECT ) - -// -// FoobarNotificationService: -// -// Service acting as the notification daemon, receiving incoming notifications from other applications. The service also -// persistently saves previously received notifications until they are closed. -// - -struct _FoobarNotificationService -{ - GObject parent_instance; - GListStore* notifications; - GtkSortListModel* sorted_notifications; - GtkFilterListModel* popup_notifications; - FoobarNotifications* skeleton; - guint bus_owner_id; - guint next_id; - gchar* cache_path; - GMutex write_cache_mutex; -}; - -enum -{ - PROP_NOTIFICATIONS = 1, - PROP_POPUP_NOTIFICATIONS, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_notification_service_class_init ( FoobarNotificationServiceClass* klass ); -static void foobar_notification_service_init ( FoobarNotificationService* self ); -static void foobar_notification_service_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_notification_service_finalize ( GObject* object ); -static void foobar_notification_service_read_cache ( FoobarNotificationService* self ); -static void foobar_notification_service_write_cache ( FoobarNotificationService* self ); -static void foobar_notification_service_write_cache_cb ( GObject* object, - GAsyncResult* result, - gpointer userdata ); -static void foobar_notification_service_write_cache_async ( FoobarNotificationService* self, - GPtrArray* notifications, - GCancellable* cancellable, - GAsyncReadyCallback callback, - gpointer userdata ); -static gboolean foobar_notification_service_write_cache_finish ( FoobarNotificationService* self, - GAsyncResult* result, - GError** error ); -static void foobar_notification_service_write_cache_thread ( GTask* task, - gpointer source_object, - gpointer task_data, - GCancellable* cancellable ); -static void foobar_notification_service_handle_bus_acquired ( GDBusConnection* connection, - gchar const* name, - gpointer userdata ); -static void foobar_notification_service_handle_bus_lost ( GDBusConnection* connection, - gchar const* name, - gpointer userdata ); -static gboolean foobar_notification_service_handle_notify ( FoobarNotifications* iface, - GDBusMethodInvocation* invocation, - gchar const* app_name, - guint replaces_id, - gchar const* image, - gchar const* summary, - gchar const* body, - gchar const* const* actions, - GVariant* hints, - gint expiration, - gpointer userdata ); -static gboolean foobar_notification_service_handle_close_notification ( FoobarNotifications* iface, - GDBusMethodInvocation* invocation, - guint id, - gpointer userdata ); -static gboolean foobar_notification_service_handle_get_capabilities ( FoobarNotifications* iface, - GDBusMethodInvocation* invocation, - gpointer userdata ); -static gboolean foobar_notification_service_handle_get_server_information( FoobarNotifications* iface, - GDBusMethodInvocation* invocation, - gpointer userdata ); -static gboolean foobar_notification_service_popup_filter_func ( gpointer item, - gpointer userdata ); -static gint foobar_notification_service_sort_func ( gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarNotificationService, foobar_notification_service, G_TYPE_OBJECT ) - -// --------------------------------------------------------------------------------------------------------------------- -// Notification Action -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for notification actions. -// -void foobar_notification_action_class_init( FoobarNotificationActionClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_notification_action_get_property; - object_klass->finalize = foobar_notification_action_finalize; - - action_props[ACTION_PROP_ID] = g_param_spec_string( - "id", - "ID", - "Textual ID of the action within the notification.", - NULL, - G_PARAM_READABLE ); - action_props[ACTION_PROP_LABEL] = g_param_spec_string( - "label", - "Label", - "Human-readable title for the action.", - NULL, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_ACTION_PROPS, action_props ); -} - -// -// Instance initialization for notification actions. -// -void foobar_notification_action_init( FoobarNotificationAction* self ) -{ - (void)self; -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_notification_action_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarNotificationAction* self = (FoobarNotificationAction*)object; - - switch ( prop_id ) - { - case ACTION_PROP_ID: - g_value_set_string( value, foobar_notification_action_get_id( self ) ); - break; - case ACTION_PROP_LABEL: - g_value_set_string( value, foobar_notification_action_get_label( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for notification actions. -// -void foobar_notification_action_finalize( GObject* object ) -{ - FoobarNotificationAction* self = (FoobarNotificationAction*)object; - - g_clear_pointer( &self->id, g_free ); - g_clear_pointer( &self->label, g_free ); - - G_OBJECT_CLASS( foobar_notification_action_parent_class )->finalize( object ); -} - -// -// Create a new notification action that is owned by the given notification (captured as an unowned reference). -// -FoobarNotificationAction* foobar_notification_action_new( FoobarNotification* notification ) -{ - FoobarNotificationAction* self = g_object_new( FOOBAR_TYPE_NOTIFICATION_ACTION, NULL ); - self->notification = notification; - return self; -} - -// -// Get the textual ID of the action within the notification. -// -gchar const* foobar_notification_action_get_id( FoobarNotificationAction* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ), NULL ); - return self->id; -} - -// -// Get a human-readable title for the action. -// -gchar const* foobar_notification_action_get_label( FoobarNotificationAction* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ), NULL ); - return self->label; -} - -// -// Update the textual ID of the action within the notification. -// -void foobar_notification_action_set_id( - FoobarNotificationAction* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ) ); - - if ( g_strcmp0( self->id, value ) ) - { - g_clear_pointer( &self->id, g_free ); - self->id = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), action_props[ACTION_PROP_ID] ); - } -} - -// -// Set a human-readable title for the action. -// -void foobar_notification_action_set_label( - FoobarNotificationAction* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ) ); - - if ( g_strcmp0( self->label, value ) ) - { - g_clear_pointer( &self->label, g_free ); - self->label = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), action_props[ACTION_PROP_LABEL] ); - } -} - -// -// Invoke this action by emitting a signal that can be handled by its sender application. -// -void foobar_notification_action_invoke( FoobarNotificationAction* self ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION_ACTION( self ) ); - - if ( self->notification && self->notification->service ) - { - FoobarNotification* notification = self->notification; - FoobarNotificationService* service = notification->service; - - foobar_notifications_emit_action_invoked( - service->skeleton, - foobar_notification_get_id( notification ), - foobar_notification_action_get_id( self ) ); - - if ( !foobar_notification_is_resident( notification ) ) - { - foobar_notification_close( notification ); - } - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Notification -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for notifications. -// -void foobar_notification_class_init( FoobarNotificationClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_notification_get_property; - object_klass->finalize = foobar_notification_finalize; - - notification_props[NOTIFICATION_PROP_ID] = g_param_spec_uint( - "id", - "ID", - "Numeric ID of the notification.", - 0, - UINT_MAX, - 0, - G_PARAM_READABLE ); - notification_props[NOTIFICATION_PROP_APP_ENTRY] = g_param_spec_string( - "app-entry", - "App Entry", - "The desktop file of the notification's application.", - NULL, - G_PARAM_READABLE ); - notification_props[NOTIFICATION_PROP_APP_NAME] = g_param_spec_string( - "app-name", - "App Name", - "Human-readable name of the application.", - NULL, - G_PARAM_READABLE ); - notification_props[NOTIFICATION_PROP_BODY] = g_param_spec_string( - "body", - "Body", - "Main content of the notification.", - NULL, - G_PARAM_READABLE ); - notification_props[NOTIFICATION_PROP_SUMMARY] = g_param_spec_string( - "summary", - "Summary", - "Brief summary of the notification's content.", - NULL, - G_PARAM_READABLE ); - notification_props[NOTIFICATION_PROP_IMAGE] = g_param_spec_object( - "image", - "Image", - "An image icon for the notification.", - GDK_TYPE_PIXBUF, - G_PARAM_READABLE ); - notification_props[NOTIFICATION_PROP_IS_DISMISSED] = g_param_spec_boolean( - "is-dismissed", - "Is Dismissed", - "Indicates that the notification was dismissed or the timeout has passed.", - FALSE, - G_PARAM_READABLE ); - notification_props[NOTIFICATION_PROP_IS_RESIDENT] = g_param_spec_boolean( - "is-resident", - "Is Resident", - "Indicates that the notification is not automatically removed once an action is invoked.", - FALSE, - G_PARAM_READABLE ); - notification_props[NOTIFICATION_PROP_IS_TRANSIENT] = g_param_spec_boolean( - "is-transient", - "Is Transient", - "Indicates that the persistence capability should be bypassed.", - FALSE, - G_PARAM_READABLE ); - notification_props[NOTIFICATION_PROP_TIME] = g_param_spec_boxed( - "time", - "Time", - "The timestamp at which the notification was received.", - G_TYPE_DATE_TIME, - G_PARAM_READABLE ); - notification_props[NOTIFICATION_PROP_TIMEOUT] = g_param_spec_int64( - "timeout", - "Timeout", - "Timeout after which the notification auto-closes in milliseconds.", - 0, - INT64_MAX, - 0, - G_PARAM_READABLE ); - notification_props[NOTIFICATION_PROP_URGENCY] = g_param_spec_enum( - "urgency", - "Urgency", - "The urgency state of the notification.", - FOOBAR_TYPE_NOTIFICATION_URGENCY, - FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_NOTIFICATION_PROPS, notification_props ); -} - -// -// Instance initialization for notifications. -// -void foobar_notification_init( FoobarNotification* self ) -{ - self->actions = g_ptr_array_new_with_free_func( foobar_notification_free_action ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_notification_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarNotification* self = (FoobarNotification*)object; - - switch ( prop_id ) - { - case NOTIFICATION_PROP_ID: - g_value_set_int64( value, foobar_notification_get_id( self ) ); - break; - case NOTIFICATION_PROP_APP_ENTRY: - g_value_set_string( value, foobar_notification_get_app_entry( self ) ); - break; - case NOTIFICATION_PROP_APP_NAME: - g_value_set_string( value, foobar_notification_get_app_name( self ) ); - break; - case NOTIFICATION_PROP_BODY: - g_value_set_string( value, foobar_notification_get_body( self ) ); - break; - case NOTIFICATION_PROP_SUMMARY: - g_value_set_string( value, foobar_notification_get_summary( self ) ); - break; - case NOTIFICATION_PROP_IMAGE: - g_value_set_object( value, foobar_notification_get_image( self ) ); - break; - case NOTIFICATION_PROP_IS_DISMISSED: - g_value_set_boolean( value, foobar_notification_is_dismissed( self ) ); - break; - case NOTIFICATION_PROP_IS_RESIDENT: - g_value_set_boolean( value, foobar_notification_is_resident( self ) ); - break; - case NOTIFICATION_PROP_IS_TRANSIENT: - g_value_set_boolean( value, foobar_notification_is_transient( self ) ); - break; - case NOTIFICATION_PROP_TIME: - g_value_set_boxed( value, foobar_notification_get_time( self ) ); - break; - case NOTIFICATION_PROP_TIMEOUT: - g_value_set_int64( value, foobar_notification_get_timeout( self ) ); - break; - case NOTIFICATION_PROP_URGENCY: - g_value_set_enum( value, foobar_notification_get_urgency( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance initialization for notifications. -// -void foobar_notification_finalize( GObject* object ) -{ - FoobarNotification* self = (FoobarNotification*)object; - - g_clear_object( &self->image ); - g_clear_pointer( &self->actions, g_ptr_array_unref ); - g_clear_pointer( &self->app_entry, g_free ); - g_clear_pointer( &self->app_name, g_free ); - g_clear_pointer( &self->body, g_free ); - g_clear_pointer( &self->summary, g_free ); - g_clear_pointer( &self->image_path, g_free ); - g_clear_pointer( &self->image_data, g_free ); - g_clear_pointer( &self->time, g_date_time_unref ); - - G_OBJECT_CLASS( foobar_notification_parent_class )->finalize( object ); -} - -// -// Create a new notification that is owned by the given service (captured as an unowned reference). -// -FoobarNotification* foobar_notification_new( FoobarNotificationService* service ) -{ - FoobarNotification* self = g_object_new( FOOBAR_TYPE_NOTIFICATION, NULL ); - self->service = service; - return self; -} - -// -// Numeric ID of the notification. -// -guint foobar_notification_get_id( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), 0 ); - return self->id; -} - -// -// A list of actions which can be invoked by notifying the notification's sender. -// -FoobarNotificationAction** foobar_notification_get_actions( - FoobarNotification* self, - guint* out_count ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); - if ( out_count ) { *out_count = self->actions->len; } - return (FoobarNotificationAction**)self->actions->pdata; -} - -// -// The desktop file of the notification's application. -// -gchar const* foobar_notification_get_app_entry( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); - return self->app_entry; -} - -// -// Human-readable name of the application. -// -gchar const* foobar_notification_get_app_name( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); - return self->app_name; -} - -// -// Main content of the notification. -// -gchar const* foobar_notification_get_body( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); - return self->body; -} - -// -// Brief summary of the notification's content. -// -gchar const* foobar_notification_get_summary( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); - return self->summary; -} - -// -// The source path for the notification's image if it was loaded from a file. -// -gchar const* foobar_notification_get_image_path( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); - return self->image_path; -} - -// -// The base64-encoded data for the notification's image if it was loaded from data. -// -gchar const* foobar_notification_get_image_data( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); - return self->image_data; -} - -// -// An image icon for the notification. -// -GdkPixbuf* foobar_notification_get_image( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); - return self->image; -} - -// -// Indicates that the notification was dismissed or the timeout has passed. -// -gboolean foobar_notification_is_dismissed( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FALSE ); - return self->is_dismissed; -} - -// -// Indicates that the notification is not automatically removed once an action is invoked. -// -gboolean foobar_notification_is_resident( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FALSE ); - return self->is_resident; -} - -// -// Indicates that the persistence capability should be bypassed. -// -gboolean foobar_notification_is_transient( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FALSE ); - return self->is_transient; -} - -// -// The timestamp at which the notification was received. -// -GDateTime* foobar_notification_get_time( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), NULL ); - return self->time; -} - -// -// Timeout after which the notification auto-closes in milliseconds. -// -gint64 foobar_notification_get_timeout( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), 0 ); - return self->timeout; -} - -// -// The urgency state of the notification. -// -FoobarNotificationUrgency foobar_notification_get_urgency( FoobarNotification* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION( self ), FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY ); - return self->urgency; -} - -// -// Numeric ID of the notification. -// -void foobar_notification_set_id( - FoobarNotification* self, - guint value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - if ( self->id != value ) - { - self->id = value; - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_ID] ); - } -} - -// -// The desktop file of the notification's application. -// -void foobar_notification_set_app_entry( - FoobarNotification* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - if ( g_strcmp0( self->app_entry, value ) ) - { - g_clear_pointer( &self->app_entry, g_free ); - self->app_entry = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_APP_ENTRY] ); - } -} - -// -// Human-readable name of the application. -// -void foobar_notification_set_app_name( - FoobarNotification* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - if ( g_strcmp0( self->app_name, value ) ) - { - g_clear_pointer( &self->app_name, g_free ); - self->app_name = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_APP_NAME] ); - } -} - -// -// Main content of the notification. -// -void foobar_notification_set_body( - FoobarNotification* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - if ( g_strcmp0( self->body, value ) ) - { - g_clear_pointer( &self->body, g_free ); - self->body = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_BODY] ); - } -} - -// -// Brief summary of the notification's content. -// -void foobar_notification_set_summary( - FoobarNotification* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - if ( g_strcmp0( self->summary, value ) ) - { - g_clear_pointer( &self->summary, g_free ); - self->summary = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_SUMMARY] ); - } -} - -// -// Update the notification's image by trying to load a file at the provided path. -// -void foobar_notification_set_image_from_path( - FoobarNotification* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - if ( g_strcmp0( self->image_path, value ) ) - { - g_clear_pointer( &self->image_path, g_free ); - g_clear_pointer( &self->image_data, g_free ); - g_clear_object( &self->image ); - self->image_path = g_strdup( value ); - - g_autoptr( GError ) error = NULL; - self->image = gdk_pixbuf_new_from_file( self->image_path, &error ); - if ( !self->image ) - { - g_warning( "Invalid image path for notification: %s", error->message ); - } - - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IMAGE] ); - } -} - -// -// Update the notification's image by trying to load base64-encoded data. -// -void foobar_notification_set_image_from_data( - FoobarNotification* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - if ( g_strcmp0( self->image_data, value ) ) - { - g_clear_pointer( &self->image_path, g_free ); - g_clear_pointer( &self->image_data, g_free ); - g_clear_object( &self->image ); - self->image_data = g_strdup( value ); - - gsize data_len; - guchar* data = g_base64_decode( self->image_data, &data_len ); - g_autoptr( GError ) error = NULL; - g_autoptr( GInputStream ) input = g_memory_input_stream_new_from_data( data, (gssize)data_len, g_free ); - self->image = gdk_pixbuf_new_from_stream( input, NULL, &error ); - if ( !self->image ) - { - g_warning( "Invalid image data for notification: %s", error->message ); - } - - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IMAGE] ); - } -} - -// -// Update the notification's image to a custom pixbuf. -// -static void foobar_notification_set_image( - FoobarNotification* self, - GdkPixbuf* value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - if ( self->image != value ) - { - g_clear_pointer( &self->image_path, g_free ); - g_clear_pointer( &self->image_data, g_free ); - g_clear_object( &self->image ); - - if ( value ) - { - self->image = g_object_ref( value ); - g_autoptr( GError ) error = NULL; - gchar* data; - gsize data_len; - if ( gdk_pixbuf_save_to_buffer( self->image, &data, &data_len, "png", &error, NULL ) ) - { - self->image_data = g_base64_encode( (guchar const*)data, data_len ); - } - else - { - g_warning( "Unable to save image for notification: %s", error->message ); - g_clear_object( &self->image ); - } - } - - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IMAGE] ); - } -} - -// -// Indicates that the notification was dismissed or the timeout has passed. -// -void foobar_notification_set_dismissed( - FoobarNotification* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - value = !!value; - if ( self->is_dismissed != value ) - { - self->is_dismissed = value; - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IS_DISMISSED] ); - - if ( self->service ) - { - gtk_filter_changed( - gtk_filter_list_model_get_filter( self->service->popup_notifications ), - value ? GTK_FILTER_CHANGE_MORE_STRICT : GTK_FILTER_CHANGE_LESS_STRICT ); - } - } -} - -// -// Indicates that the notification is not automatically removed once an action is invoked. -// -void foobar_notification_set_resident( - FoobarNotification* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - value = !!value; - if ( self->is_resident != value ) - { - self->is_resident = value; - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IS_RESIDENT] ); - } -} - -// -// Indicates that the persistence capability should be bypassed. -// -void foobar_notification_set_transient( - FoobarNotification* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - value = !!value; - if ( self->is_transient != value ) - { - self->is_transient = value; - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_IS_TRANSIENT] ); - } -} - -// -// The timestamp at which the notification was received. -// -void foobar_notification_set_time( - FoobarNotification* self, - GDateTime* value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - if ( self->time != value ) - { - if ( self->time ) { g_date_time_unref( self->time ); } - self->time = g_date_time_ref( value ); - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_TIME] ); - } -} - -// -// Timeout after which the notification auto-closes in milliseconds. -// -void foobar_notification_set_timeout( - FoobarNotification* self, - gint64 value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - value = MAX( value, 0 ); - if ( self->timeout != value ) - { - self->timeout = value; - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_TIMEOUT] ); - } -} - -// -// The urgency state of the notification. -// -void foobar_notification_set_urgency( - FoobarNotification* self, - FoobarNotificationUrgency value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - if ( self->urgency != value ) - { - self->urgency = value; - g_object_notify_by_pspec( G_OBJECT( self ), notification_props[NOTIFICATION_PROP_URGENCY] ); - } -} - -// -// Register an action for the notification. -// -void foobar_notification_add_action( - FoobarNotification* self, - FoobarNotificationAction* action ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - g_return_if_fail( action->notification == self ); - - g_ptr_array_add( self->actions, g_object_ref( action ) ); -} - -// -// Block the notification from automatically being dismissed. -// -void foobar_notification_block_timeout( FoobarNotification* self ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - g_clear_handle_id( &self->timeout_id, g_source_remove ); -} - -// -// If not already started, start the timeout to automatically dismiss the notification after it's timeout. -// -void foobar_notification_resume_timeout( FoobarNotification* self ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - if ( !self->is_dismissed && !self->timeout_id ) - { - self->timeout_id = g_timeout_add_full( - G_NOTIFICATION_PRIORITY_NORMAL, - self->timeout, - foobar_notification_handle_timeout, - g_object_ref( self ), - g_object_unref ); - } -} - -// -// Manually dismiss the notification, setting "is-dismissed" to TRUE. -// -void foobar_notification_dismiss( FoobarNotification* self ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - foobar_notification_set_dismissed( self, TRUE ); -} - -// -// Close the notification, removing it from the parent service's list. -// -void foobar_notification_close( FoobarNotification* self ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION( self ) ); - - if ( self->service ) - { - FoobarNotificationService* service = self->service; - self->service = NULL; - - guint id = foobar_notification_get_id( self ); - - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( service->notifications ) ); ++i ) - { - if ( g_list_model_get_item( G_LIST_MODEL( service->notifications ), i ) == self ) - { - g_list_store_remove( service->notifications, i ); - foobar_notification_service_write_cache( service ); - break; - } - } - - foobar_notifications_emit_notification_closed( service->skeleton, id, 3 ); - } -} - -// -// Called when an action is removed from a notification, resetting its weak reference to the notification. -// -void foobar_notification_free_action( gpointer action ) -{ - FoobarNotificationAction* object = (FoobarNotificationAction*)action; - object->notification = NULL; - g_object_unref( object ); -} - -// -// Called after the notification's timeout has elapsed. -// -gboolean foobar_notification_handle_timeout( gpointer userdata ) -{ - FoobarNotification* notification = (FoobarNotification*)userdata; - - foobar_notification_dismiss( notification ); - notification->timeout_id = 0; - - return G_SOURCE_REMOVE; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Service Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the notification service. -// -void foobar_notification_service_class_init( FoobarNotificationServiceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_notification_service_get_property; - object_klass->finalize = foobar_notification_service_finalize; - - props[PROP_NOTIFICATIONS] = g_param_spec_object( - "notifications", - "Notifications", - "Sorted list of all notifications.", - G_TYPE_LIST_MODEL, - G_PARAM_READABLE ); - props[PROP_POPUP_NOTIFICATIONS] = g_param_spec_object( - "popup-notifications", - "Popup Notifications", - "Sorted list of all visible notifications (i.e. notifications that are not dismissed).", - G_TYPE_LIST_MODEL, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for the notification service. -// -void foobar_notification_service_init( FoobarNotificationService* self ) -{ - self->notifications = g_list_store_new( FOOBAR_TYPE_NOTIFICATION ); - self->cache_path = foobar_get_cache_path( "notifications.json" ); - g_mutex_init( &self->write_cache_mutex ); - - GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_notification_service_sort_func, NULL, NULL ); - self->sorted_notifications = gtk_sort_list_model_new( - G_LIST_MODEL( g_object_ref( self->notifications ) ), - GTK_SORTER( sorter ) ); - - GtkCustomFilter* popup_filter = gtk_custom_filter_new( foobar_notification_service_popup_filter_func, NULL, NULL ); - self->popup_notifications = gtk_filter_list_model_new( - G_LIST_MODEL( g_object_ref( self->sorted_notifications ) ), - GTK_FILTER( popup_filter ) ); - - foobar_notification_service_read_cache( self ); - - self->bus_owner_id = g_bus_own_name( - G_BUS_TYPE_SESSION, - "org.freedesktop.Notifications", - G_BUS_NAME_OWNER_FLAGS_NONE, - foobar_notification_service_handle_bus_acquired, - NULL, - foobar_notification_service_handle_bus_lost, - self, - NULL ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_notification_service_get_property( GObject* object, guint prop_id, GValue* value, GParamSpec* pspec ) -{ - FoobarNotificationService* self = (FoobarNotificationService*)object; - - switch ( prop_id ) - { - case PROP_NOTIFICATIONS: - g_value_set_object( value, foobar_notification_service_get_notifications( self ) ); - break; - case PROP_POPUP_NOTIFICATIONS: - g_value_set_object( value, foobar_notification_service_get_popup_notifications( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for the notification service. -// -void foobar_notification_service_finalize( GObject* object ) -{ - FoobarNotificationService* self = (FoobarNotificationService*)object; - - if ( self->skeleton ) { g_dbus_interface_skeleton_unexport( G_DBUS_INTERFACE_SKELETON( self->skeleton ) ); } - - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->notifications ) ); ++i ) - { - FoobarNotification* notification = g_list_model_get_item( G_LIST_MODEL( self->notifications ), i ); - notification->service = NULL; - } - - g_clear_object( &self->popup_notifications ); - g_clear_object( &self->sorted_notifications ); - g_clear_object( &self->notifications ); - g_clear_object( &self->skeleton ); - g_clear_handle_id( &self->bus_owner_id, g_bus_unown_name ); - g_clear_pointer( &self->cache_path, g_free ); - - g_mutex_clear( &self->write_cache_mutex ); - - G_OBJECT_CLASS( foobar_notification_service_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new notification service instance. -// -FoobarNotificationService* foobar_notification_service_new( void ) -{ - return g_object_new( FOOBAR_TYPE_NOTIFICATION_SERVICE, NULL ); -} - -// -// Get a sorted list of all notifications. -// -GListModel* foobar_notification_service_get_notifications( FoobarNotificationService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( self ), NULL ); - return G_LIST_MODEL( self->sorted_notifications ); -} - -// -// Get a sorted list of all visible notifications (i.e. notifications that are not dismissed). -// -GListModel* foobar_notification_service_get_popup_notifications( FoobarNotificationService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_SERVICE( self ), NULL ); - return G_LIST_MODEL( self->popup_notifications ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called after the process has asynchronously acquired the notification bus. -// -// This is where we can export notification daemon functionality using the corresponding DBus skeleton object. -// -void foobar_notification_service_handle_bus_acquired( - GDBusConnection* connection, - gchar const* name, - gpointer userdata ) -{ - (void)name; - FoobarNotificationService* self = (FoobarNotificationService*)userdata; - - self->skeleton = foobar_notifications_skeleton_new( ); - g_signal_connect( - self->skeleton, - "handle-notify", - G_CALLBACK( foobar_notification_service_handle_notify ), - self ); - g_signal_connect( - self->skeleton, - "handle-close-notification", - G_CALLBACK( foobar_notification_service_handle_close_notification ), - self ); - g_signal_connect( - self->skeleton, - "handle-get-capabilities", - G_CALLBACK( foobar_notification_service_handle_get_capabilities ), - self ); - g_signal_connect( - self->skeleton, - "handle-get-server-information", - G_CALLBACK( foobar_notification_service_handle_get_server_information ), - self ); - - g_autoptr( GError ) error = NULL; - if ( !g_dbus_interface_skeleton_export( - G_DBUS_INTERFACE_SKELETON( self->skeleton ), - connection, - "/org/freedesktop/Notifications", - &error ) ) - { - g_warning( "Unable to export notifications interface: %s", error->message ); - } -} - -// -// Called after the process has failed to acquire the notification bus. -// -void foobar_notification_service_handle_bus_lost( - GDBusConnection* connection, - gchar const* name, - gpointer userdata ) -{ - (void)connection; - (void)name; - (void)userdata; - - g_autofree gchar* other_name = NULL; - g_autoptr( FoobarNotifications ) proxy = foobar_notifications_proxy_new_for_bus_sync( - G_BUS_TYPE_SESSION, - G_DBUS_PROXY_FLAGS_NONE, - "org.freedesktop.Notifications", - "/org/freedesktop/Notifications", - NULL, - NULL ); - - if ( proxy ) - { - foobar_notifications_call_get_server_information_sync( - proxy, - &other_name, - NULL, - NULL, - NULL, - NULL, - NULL ); - } - - g_warning( - "Unable to register notification daemon because %s is already running.", - other_name ? other_name : "(unknown)" ); -} - -// -// DBus skeleton callback for the "Notify" method. -// -gboolean foobar_notification_service_handle_notify( - FoobarNotifications* iface, - GDBusMethodInvocation* invocation, - gchar const* app_name, - guint replaces_id, - gchar const* image, - gchar const* summary, - gchar const* body, - gchar const* const* actions, - GVariant* hints, - gint expiration, - gpointer userdata ) -{ - FoobarNotificationService* self = (FoobarNotificationService*)userdata; - - g_autoptr( GDateTime ) time = g_date_time_new_now_local( ); - g_autoptr( FoobarNotification ) notification = foobar_notification_new( self ); - foobar_notification_set_id( notification, replaces_id ? replaces_id : self->next_id++ ); - foobar_notification_set_app_name( notification, app_name ); - foobar_notification_set_summary( notification, summary ); - foobar_notification_set_body( notification, body ); - foobar_notification_set_time( notification, time ); - foobar_notification_set_timeout( notification, expiration != -1 ? expiration : DEFAULT_TIMEOUT ); - if ( image && *image ) { foobar_notification_set_image_from_path( notification, image ); } - - if ( hints ) - { - // Process hints to set some more properties for the notification. - - gsize hint_count = g_variant_n_children( hints ); - for ( gsize i = 0; i < hint_count; ++i ) - { - g_autoptr( GVariant ) hint = g_variant_get_child_value( hints, i ); - g_autoptr( GVariant ) key = g_variant_get_child_value( hint, 0 ); - g_autoptr( GVariant ) value_wrapper = g_variant_get_child_value( hint, 1 ); - g_autoptr( GVariant ) value = g_variant_get_variant( value_wrapper ); - gchar const* key_str = g_variant_get_string( key, NULL ); - - if ( !g_strcmp0( key_str, "desktop-entry" ) ) - { - foobar_notification_set_app_entry( notification, g_variant_get_string( value, NULL ) ); - } - else if ( !g_strcmp0( key_str, "resident" ) ) - { - foobar_notification_set_resident( notification, g_variant_get_boolean( value ) ); - } - else if ( !g_strcmp0( key_str, "transient" ) ) - { - foobar_notification_set_transient( notification, g_variant_get_boolean( value ) ); - } - else if ( !g_strcmp0( key_str, "urgency" ) ) - { - foobar_notification_set_urgency( notification, g_variant_get_byte( value ) ); - } - - if ( !foobar_notification_get_image( notification ) ) - { - if ( !g_strcmp0( key_str, "image-path" ) || - !g_strcmp0( key_str, "image_path" ) ) - { - foobar_notification_set_image_from_path( notification, g_variant_get_string( value, NULL ) ); - } - else if ( !g_strcmp0( key_str, "image-data" ) || - !g_strcmp0( key_str, "image_data" ) || - !g_strcmp0( key_str, "icon_data" ) ) - { - gint32 width, height, row_stride, bits_per_sample, channels; - gboolean has_alpha; - g_variant_get( - value, - "(iiibiiay)", - &width, - &height, - &row_stride, - &has_alpha, - &bits_per_sample, - &channels, - NULL ); - - g_autoptr( GVariant ) data_variant = g_variant_get_child_value( value, 6 ); - gsize data_len; - guint8 const* data = g_variant_get_fixed_array( data_variant, &data_len, sizeof( guint8 ) ); - g_autoptr( GBytes ) bytes = g_bytes_new( data, data_len ); - - g_autoptr( GdkPixbuf ) pixbuf = gdk_pixbuf_new_from_bytes( - bytes, - GDK_COLORSPACE_RGB, - has_alpha, - bits_per_sample, - width, - height, - row_stride ); - foobar_notification_set_image( notification, pixbuf ); - } - } - } - } - - if ( actions ) - { - // Actions are stored in a flat array containing the label followed by the ID of the action. - - while ( *actions ) - { - gchar const* label = *actions++; - gchar const* id = *actions++; - if ( !id ) - { - g_warning( "Action missing id: %s", label ); - break; - } - - g_autoptr( FoobarNotificationAction ) action = foobar_notification_action_new( notification ); - foobar_notification_action_set_id( action, id ); - foobar_notification_action_set_label( action, label ); - foobar_notification_add_action( notification, action ); - } - } - - // Add the notification to the list and start the timeout. - - g_list_store_append( self->notifications, notification ); - foobar_notification_resume_timeout( notification ); - foobar_notification_service_write_cache( self ); - - foobar_notifications_complete_notify( iface, invocation, foobar_notification_get_id( notification ) ); - return G_DBUS_METHOD_INVOCATION_HANDLED; -} - -// -// DBus skeleton callback for the "CloseNotification" method. -// -gboolean foobar_notification_service_handle_close_notification( - FoobarNotifications* iface, - GDBusMethodInvocation* invocation, - guint id, - gpointer userdata ) -{ - FoobarNotificationService* self = (FoobarNotificationService*)userdata; - - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->notifications ) ); ++i ) - { - FoobarNotification* notification = g_list_model_get_item( G_LIST_MODEL( self->notifications ), i ); - if ( foobar_notification_get_id( notification ) == id ) - { - foobar_notification_close( notification ); - break; - } - } - - foobar_notifications_complete_close_notification( iface, invocation ); - return G_DBUS_METHOD_INVOCATION_HANDLED; -} - -// -// DBus skeleton callback for the "GetCapabilities" method. -// -gboolean foobar_notification_service_handle_get_capabilities( - FoobarNotifications* iface, - GDBusMethodInvocation* invocation, - gpointer userdata ) -{ - (void)userdata; - - gchar const* capabilities[] = - { - "action-icons", - "actions", - "body", - "body-hyperlinks", - "body-markup", - "icon-static", - "persistence", - NULL - }; - foobar_notifications_complete_get_capabilities( iface, invocation, capabilities ); - return G_DBUS_METHOD_INVOCATION_HANDLED; -} - -// -// DBus skeleton callback for the "GetServerInformation" method. -// -gboolean foobar_notification_service_handle_get_server_information( - FoobarNotifications* iface, - GDBusMethodInvocation* invocation, - gpointer userdata ) -{ - (void)userdata; - - foobar_notifications_complete_get_server_information( - iface, - invocation, - "Foobar", - "Hannes Schulze", - "1.0.0", - "1.2" ); - return G_DBUS_METHOD_INVOCATION_HANDLED; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Methods -// --------------------------------------------------------------------------------------------------------------------- - -// -// Synchronously read the cache file at self->cache_path, populating self->notifications and initializing self->next_id. -// -void foobar_notification_service_read_cache( FoobarNotificationService* self ) -{ - g_autoptr( GError ) error = NULL; - - if ( self->cache_path && g_file_test( self->cache_path, G_FILE_TEST_EXISTS ) ) - { - g_autoptr( JsonParser ) parser = json_parser_new( ); - if ( !json_parser_load_from_mapped_file( parser, self->cache_path, &error ) ) - { - g_warning( "Unable to read cached notifications: %s", error->message ); - return; - } - - JsonNode* root_node = json_parser_get_root( parser ); - JsonArray* root_array = json_node_get_array( root_node ); - for ( guint i = 0; i < json_array_get_length( root_array ); ++i ) - { - JsonObject* notification_object = json_array_get_object_element( root_array, i ); - g_autoptr( FoobarNotification ) notification = foobar_notification_new( self ); - - foobar_notification_set_dismissed( notification, TRUE ); - - guint id = json_object_get_int_member( notification_object, "id" ); - foobar_notification_set_id( notification, id ); - - gchar const* app_entry = json_object_get_string_member_with_default( notification_object, "app-entry", NULL ); - foobar_notification_set_app_entry( notification, app_entry ); - - gchar const* app_name = json_object_get_string_member_with_default( notification_object, "app-name", NULL ); - foobar_notification_set_app_name( notification, app_name ); - - gchar const* body = json_object_get_string_member_with_default( notification_object, "body", NULL ); - foobar_notification_set_body( notification, body ); - - gchar const* summary = json_object_get_string_member_with_default( notification_object, "summary", NULL ); - foobar_notification_set_summary( notification, summary ); - - gchar const* image_path = json_object_get_string_member_with_default( notification_object, "image-path", NULL ); - gchar const* image_data = json_object_get_string_member_with_default( notification_object, "image-data", NULL ); - if ( image_path ) { foobar_notification_set_image_from_path( notification, image_path ); } - else if ( image_data ) { foobar_notification_set_image_from_data( notification, image_data ); } - - gboolean is_resident = json_object_get_boolean_member( notification_object, "is-resident" ); - foobar_notification_set_resident( notification, is_resident ); - - gchar const* time_str = json_object_get_string_member_with_default( notification_object, "time", NULL ); - GTimeZone* tz = g_time_zone_new_local( ); - GDateTime* time = time_str ? g_date_time_new_from_iso8601( time_str, tz ) : NULL; - foobar_notification_set_time( notification, time ); - g_time_zone_unref( tz ); - if ( time ) { g_date_time_unref( time ); } - - gint64 timeout = json_object_get_int_member( notification_object, "timeout" ); - foobar_notification_set_timeout( notification, timeout ); - - FoobarNotificationUrgency urgency = json_object_get_int_member( notification_object, "urgency" ); - foobar_notification_set_urgency( notification, urgency ); - - JsonArray* actions_array = json_object_get_array_member( notification_object, "actions" ); - for ( guint j = 0; j < json_array_get_length( actions_array ); ++j ) - { - JsonObject* action_object = json_array_get_object_element( actions_array, j ); - g_autoptr( FoobarNotificationAction ) action = foobar_notification_action_new( notification ); - - gchar const* action_id = json_object_get_string_member_with_default( action_object, "id", NULL ); - foobar_notification_action_set_id( action, action_id ); - - gchar const* label = json_object_get_string_member_with_default( action_object, "label", NULL ); - foobar_notification_action_set_label( action, label ); - - foobar_notification_add_action( notification, action ); - } - - g_list_store_append( self->notifications, notification ); - self->next_id = MAX( self->next_id, id + 1 ); - } - } -} - -// -// Asynchronously start writing the cache file at self->cache_path. -// -void foobar_notification_service_write_cache( FoobarNotificationService* self ) -{ - guint count = g_list_model_get_n_items( G_LIST_MODEL( self->notifications ) ); - g_autoptr( GPtrArray ) notifications = g_ptr_array_new_full( count, g_object_unref ); - for ( guint i = 0; i < count; ++i ) - { - FoobarNotification* notification = g_list_model_get_item( G_LIST_MODEL( self->notifications ), i ); - if ( !foobar_notification_is_transient( notification ) ) - { - g_ptr_array_add( notifications, g_object_ref( notification ) ); - } - } - - foobar_notification_service_write_cache_async( - self, - g_steal_pointer( ¬ifications ), - NULL, - foobar_notification_service_write_cache_cb, - NULL ); -} - -// -// Callback invoked when the cache was written successfully or failed. -// -void foobar_notification_service_write_cache_cb( - GObject* object, - GAsyncResult* result, - gpointer userdata ) -{ - (void)userdata; - FoobarNotificationService* self = (FoobarNotificationService*)object; - - g_autoptr( GError ) error = NULL; - if ( !foobar_notification_service_write_cache_finish( self, result, &error ) ) - { - g_warning( "Unable to write cached notifications: %s", error->message ); - } -} - -// -// Asynchronously write the cache file at self->cache_path, using the provided snapshot of the list. -// -void foobar_notification_service_write_cache_async( - FoobarNotificationService* self, - GPtrArray* notifications, - GCancellable* cancellable, - GAsyncReadyCallback callback, - gpointer userdata ) -{ - g_autoptr( GTask ) task = g_task_new( self, cancellable, callback, userdata ); - g_task_set_name( task, "write-notification-cache" ); - g_task_set_task_data( task, notifications, (GDestroyNotify)g_ptr_array_unref ); - g_task_run_in_thread( task, foobar_notification_service_write_cache_thread ); -} - -// -// Get the asynchronous result for writing the cache file, returning TRUE on success, or FALSE on error. -// -gboolean foobar_notification_service_write_cache_finish( - FoobarNotificationService* self, - GAsyncResult* result, - GError** error ) -{ - (void)self; - - return g_task_propagate_boolean( G_TASK( result ), error ); -} - -// -// Task implementation for foobar_notification_service_write_cache_async, invoked on a background thread. -// -void foobar_notification_service_write_cache_thread( - GTask* task, - gpointer source_object, - gpointer task_data, - GCancellable* cancellable ) -{ - (void)cancellable; - FoobarNotificationService* self = (FoobarNotificationService*)source_object; - GPtrArray* notifications = (GPtrArray*)task_data; - - if ( !self->cache_path ) - { - g_task_return_boolean( task, TRUE ); - return; - } - - g_autoptr( GError ) error = NULL; - - g_autoptr( JsonBuilder ) builder = json_builder_new( ); - json_builder_begin_array( builder ); - for ( guint i = 0; i < notifications->len; ++i ) - { - // Only init-only properties are read here so no extra synchronization is needed. - - FoobarNotification* notification = g_ptr_array_index( notifications, i ); - - json_builder_begin_object( builder ); - - json_builder_set_member_name( builder, "id" ); - json_builder_add_int_value( builder, foobar_notification_get_id( notification ) ); - - json_builder_set_member_name( builder, "app-entry" ); - json_builder_add_string_value( builder, foobar_notification_get_app_entry( notification ) ); - - json_builder_set_member_name( builder, "app-name" ); - json_builder_add_string_value( builder, foobar_notification_get_app_name( notification ) ); - - json_builder_set_member_name( builder, "body" ); - json_builder_add_string_value( builder, foobar_notification_get_body( notification ) ); - - json_builder_set_member_name( builder, "summary" ); - json_builder_add_string_value( builder, foobar_notification_get_summary( notification ) ); - - json_builder_set_member_name( builder, "image-path" ); - json_builder_add_string_value( builder, foobar_notification_get_image_path( notification ) ); - - json_builder_set_member_name( builder, "image-data" ); - json_builder_add_string_value( builder, foobar_notification_get_image_data( notification ) ); - - json_builder_set_member_name( builder, "is-resident" ); - json_builder_add_boolean_value( builder, foobar_notification_is_resident( notification ) ); - - GDateTime* time = foobar_notification_get_time( notification ); - gchar* time_str = time ? g_date_time_format_iso8601( time ) : NULL; - json_builder_set_member_name( builder, "time" ); - json_builder_add_string_value( builder, time_str ); - g_free( time_str ); - - json_builder_set_member_name( builder, "timeout" ); - json_builder_add_int_value( builder, foobar_notification_get_timeout( notification ) ); - - json_builder_set_member_name( builder, "urgency" ); - json_builder_add_int_value( builder, foobar_notification_get_urgency( notification ) ); - - guint action_count; - FoobarNotificationAction** actions = foobar_notification_get_actions( notification, &action_count ); - json_builder_set_member_name( builder, "actions" ); - json_builder_begin_array( builder ); - for ( guint j = 0; j < action_count; ++j ) - { - FoobarNotificationAction* action = actions[j]; - - json_builder_begin_object( builder ); - - json_builder_set_member_name( builder, "id" ); - json_builder_add_string_value( builder, foobar_notification_action_get_id( action ) ); - - json_builder_set_member_name( builder, "label" ); - json_builder_add_string_value( builder, foobar_notification_action_get_label( action ) ); - - json_builder_end_object( builder ); - } - json_builder_end_array( builder ); - - json_builder_end_object( builder ); - } - json_builder_end_array( builder ); - - g_autoptr( JsonNode ) root_node = json_builder_get_root( builder ); - g_autoptr( JsonGenerator ) generator = json_generator_new( ); - json_generator_set_root( generator, root_node ); - - g_mutex_lock( &self->write_cache_mutex ); - gboolean success = json_generator_to_file( generator, self->cache_path, &error ); - g_mutex_unlock( &self->write_cache_mutex ); - if ( !success ) - { - g_task_return_error( task, g_steal_pointer( &error ) ); - return; - } - - g_task_return_boolean( task, TRUE ); -} - -// -// Filtering callback for the list of pop-up notifications. -// -gboolean foobar_notification_service_popup_filter_func( - gpointer item, - gpointer userdata ) -{ - (void)userdata; - - FoobarNotification* notification = item; - return !foobar_notification_is_dismissed( notification ); -} - -// -// Sorting callback for the list of notifications. -// -// Notifications are sorted by timestamp in descending order. -// -gint foobar_notification_service_sort_func( - gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ) -{ - (void)userdata; - - FoobarNotification* notification_a = (FoobarNotification*)item_a; - FoobarNotification* notification_b = (FoobarNotification*)item_b; - return -g_date_time_compare( - foobar_notification_get_time( notification_a ), - foobar_notification_get_time( notification_b ) ); -} diff --git a/src/services/notification-service.h b/src/services/notification-service.h deleted file mode 100644 index abcca4e..0000000 --- a/src/services/notification-service.h +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once - -#include -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_NOTIFICATION_URGENCY foobar_notification_urgency_get_type( ) -#define FOOBAR_TYPE_NOTIFICATION_ACTION foobar_notification_action_get_type( ) -#define FOOBAR_TYPE_NOTIFICATION foobar_notification_get_type( ) -#define FOOBAR_TYPE_NOTIFICATION_SERVICE foobar_notification_service_get_type( ) - -typedef enum -{ - // reminder to myself that i should have studied this shit instead of writing a fucking bar in c - FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_COMFORT = 0, - FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_MONEY = 1, - FOOBAR_NOTIFICATION_URGENCY_LOSS_OF_LIFE = 2, -} FoobarNotificationUrgency; - -GType foobar_notification_urgency_get_type( void ); - -G_DECLARE_FINAL_TYPE( FoobarNotificationAction, foobar_notification_action, FOOBAR, NOTIFICATION_ACTION, GObject ) - -gchar const* foobar_notification_action_get_id ( FoobarNotificationAction* self ); -gchar const* foobar_notification_action_get_label( FoobarNotificationAction* self ); -void foobar_notification_action_invoke ( FoobarNotificationAction* self ); - -G_DECLARE_FINAL_TYPE( FoobarNotification, foobar_notification, FOOBAR, NOTIFICATION, GObject ) - -guint foobar_notification_get_id ( FoobarNotification* self ); -FoobarNotificationAction** foobar_notification_get_actions ( FoobarNotification* self, - guint* out_count ); -gchar const* foobar_notification_get_app_entry ( FoobarNotification* self ); -gchar const* foobar_notification_get_app_name ( FoobarNotification* self ); -gchar const* foobar_notification_get_body ( FoobarNotification* self ); -gchar const* foobar_notification_get_summary ( FoobarNotification* self ); -GdkPixbuf* foobar_notification_get_image ( FoobarNotification* self ); -gboolean foobar_notification_is_dismissed ( FoobarNotification* self ); -gboolean foobar_notification_is_resident ( FoobarNotification* self ); -gboolean foobar_notification_is_transient ( FoobarNotification* self ); -GDateTime* foobar_notification_get_time ( FoobarNotification* self ); -gint64 foobar_notification_get_timeout ( FoobarNotification* self ); -FoobarNotificationUrgency foobar_notification_get_urgency ( FoobarNotification* self ); -void foobar_notification_block_timeout ( FoobarNotification* self ); -void foobar_notification_resume_timeout( FoobarNotification* self ); -void foobar_notification_dismiss ( FoobarNotification* self ); -void foobar_notification_close ( FoobarNotification* self ); - -G_DECLARE_FINAL_TYPE( FoobarNotificationService, foobar_notification_service, FOOBAR, NOTIFICATION_SERVICE, GObject ) - -FoobarNotificationService* foobar_notification_service_new ( void ); -GListModel* foobar_notification_service_get_notifications ( FoobarNotificationService* self ); -GListModel* foobar_notification_service_get_popup_notifications( FoobarNotificationService* self ); - -G_END_DECLS diff --git a/src/services/quick-answer-service.c b/src/services/quick-answer-service.c deleted file mode 100644 index 670a72f..0000000 --- a/src/services/quick-answer-service.c +++ /dev/null @@ -1,360 +0,0 @@ -#include "services/quick-answer-service.h" -#include "services/quick-answers/math.h" -#include "launcher-item.h" -#include - -// -// FoobarQuickAnswer: -// -// A launcher item representing a quick, one-line answer to the query entered by the user. -// - -struct _FoobarQuickAnswer -{ - GObject parent_instance; - gchar* title; - gchar* value; - GIcon* icon; -}; - -enum -{ - ANSWER_PROP_VALUE = 1, - ANSWER_PROP_TITLE, - ANSWER_PROP_DESCRIPTION, - ANSWER_PROP_ICON, - N_ANSWER_PROPS, -}; - -static GParamSpec* answer_props[N_ANSWER_PROPS] = { 0 }; - -static void foobar_quick_answer_class_init ( FoobarQuickAnswerClass* klass ); -static void foobar_quick_answer_launcher_item_interface_init( FoobarLauncherItemInterface* iface ); -static void foobar_quick_answer_init ( FoobarQuickAnswer* self ); -static void foobar_quick_answer_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_quick_answer_finalize ( GObject* object ); -static FoobarQuickAnswer* foobar_quick_answer_new ( void ); -static gchar const* foobar_quick_answer_get_title ( FoobarLauncherItem* self ); -static gchar const* foobar_quick_answer_get_description ( FoobarLauncherItem* self ); -static GIcon* foobar_quick_answer_get_icon ( FoobarLauncherItem* self ); -static void foobar_quick_answer_activate ( FoobarLauncherItem* self ); -static void foobar_quick_answer_set_title ( FoobarQuickAnswer* self, - gchar const* value ); -static void foobar_quick_answer_set_value ( FoobarQuickAnswer* self, - gchar const* value ); -static void foobar_quick_answer_set_icon ( FoobarQuickAnswer* self, - GIcon* value ); - -G_DEFINE_FINAL_TYPE_WITH_CODE( - FoobarQuickAnswer, - foobar_quick_answer, - G_TYPE_OBJECT, - G_IMPLEMENT_INTERFACE( FOOBAR_TYPE_LAUNCHER_ITEM, foobar_quick_answer_launcher_item_interface_init ) ) - -// -// FoobarQuickAnswerService: -// -// Service providing quick answers for search queries in the launcher (if available). This includes evaluating -// mathematical expressions. -// - -struct _FoobarQuickAnswerService -{ - GObject parent_instance; -}; - -static void foobar_quick_answer_service_class_init( FoobarQuickAnswerServiceClass* klass ); -static void foobar_quick_answer_service_init ( FoobarQuickAnswerService* self ); -static FoobarQuickAnswer* foobar_quick_answer_service_query_math( gchar const* query ); - -G_DEFINE_FINAL_TYPE( FoobarQuickAnswerService, foobar_quick_answer_service, G_TYPE_OBJECT ) - -// --------------------------------------------------------------------------------------------------------------------- -// Quick Answers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for quick answers. -// -void foobar_quick_answer_class_init( FoobarQuickAnswerClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_quick_answer_get_property; - object_klass->finalize = foobar_quick_answer_finalize; - - gpointer launcher_item_iface = g_type_default_interface_peek( FOOBAR_TYPE_LAUNCHER_ITEM ); - answer_props[ANSWER_PROP_VALUE] = g_param_spec_string( - "value", - "Value", - "The raw value of the quick answer.", - NULL, - G_PARAM_READABLE ); - answer_props[ANSWER_PROP_TITLE] = g_param_spec_override( - "title", - g_object_interface_find_property( launcher_item_iface, "title" ) ); - answer_props[ANSWER_PROP_DESCRIPTION] = g_param_spec_override( - "description", - g_object_interface_find_property( launcher_item_iface, "description" ) ); - answer_props[ANSWER_PROP_ICON] = g_param_spec_override( - "icon", - g_object_interface_find_property( launcher_item_iface, "icon" ) ); - g_object_class_install_properties( object_klass, N_ANSWER_PROPS, answer_props ); -} - -// -// Static initialization of the FoobarLauncherItem interface. -// -void foobar_quick_answer_launcher_item_interface_init( FoobarLauncherItemInterface* iface ) -{ - iface->get_title = foobar_quick_answer_get_title; - iface->get_description = foobar_quick_answer_get_description; - iface->get_icon = foobar_quick_answer_get_icon; - iface->activate = foobar_quick_answer_activate; -} - -// -// Instance initialization for quick answers. -// -void foobar_quick_answer_init( FoobarQuickAnswer* self ) -{ - (void)self; -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_quick_answer_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarQuickAnswer* self = (FoobarQuickAnswer*)object; - - switch ( prop_id ) - { - case ANSWER_PROP_VALUE: - g_value_set_string( value, foobar_quick_answer_get_value( self ) ); - break; - case ANSWER_PROP_TITLE: - g_value_set_string( value, foobar_launcher_item_get_title( FOOBAR_LAUNCHER_ITEM ( self ) ) ); - break; - case ANSWER_PROP_DESCRIPTION: - g_value_set_string( value, foobar_launcher_item_get_description( FOOBAR_LAUNCHER_ITEM( self ) ) ); - break; - case ANSWER_PROP_ICON: - g_value_set_object( value, foobar_launcher_item_get_icon( FOOBAR_LAUNCHER_ITEM( self ) ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for quick answers. -// -void foobar_quick_answer_finalize( GObject* object ) -{ - FoobarQuickAnswer* self = (FoobarQuickAnswer*)object; - - g_clear_pointer( &self->title, g_free ); - g_clear_pointer( &self->value, g_free ); - g_clear_object( &self->icon ); - - G_OBJECT_CLASS( foobar_quick_answer_parent_class )->finalize( object ); -} - -// -// Create a new, default-initialized quick answer. -// -FoobarQuickAnswer* foobar_quick_answer_new( void ) -{ - return g_object_new( FOOBAR_TYPE_QUICK_ANSWER, NULL ); -} - -// -// Get the raw value of the quick answer. -// -gchar const* foobar_quick_answer_get_value( FoobarQuickAnswer* self ) -{ - g_return_val_if_fail( FOOBAR_IS_QUICK_ANSWER( self ), NULL ); - return self->value; -} - -// -// Get the title for the quick answer. -// -gchar const* foobar_quick_answer_get_title( FoobarLauncherItem* item ) -{ - FoobarQuickAnswer* self = ( FoobarQuickAnswer* )item; - return self->title; -} - -// -// Get the description shown below the title, always NULL for quick answers. -// -gchar const* foobar_quick_answer_get_description( FoobarLauncherItem* item ) -{ - (void)item; - return NULL; -} - -// -// Get an icon representing the quick answer. -// -GIcon* foobar_quick_answer_get_icon( FoobarLauncherItem* item ) -{ - FoobarQuickAnswer* self = ( FoobarQuickAnswer* )item; - return self->icon; -} - -// -// Copy the quick answer's value into the clipboard. -// -void foobar_quick_answer_activate( FoobarLauncherItem* item ) -{ - FoobarQuickAnswer* self = ( FoobarQuickAnswer* )item; - if ( self->value ) - { - GdkClipboard* clipboard = gdk_display_get_clipboard( gdk_display_get_default( ) ); - gdk_clipboard_set_text( clipboard, self->value ); - } -} - -// -// Update the raw value of the quick answer. -// -void foobar_quick_answer_set_value( - FoobarQuickAnswer* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_QUICK_ANSWER( self ) ); - - if ( g_strcmp0( self->value, value ) ) - { - g_clear_pointer( &self->value, g_free ); - self->value = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), answer_props[ANSWER_PROP_VALUE] ); - } -} - -// -// Update the title for the quick answer. -// -void foobar_quick_answer_set_title( - FoobarQuickAnswer* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_QUICK_ANSWER( self ) ); - - if ( g_strcmp0( self->title, value ) ) - { - g_clear_pointer( &self->title, g_free ); - self->title = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), answer_props[ANSWER_PROP_TITLE] ); - } -} - -// -// Update the icon representing the quick answer. -// -void foobar_quick_answer_set_icon( - FoobarQuickAnswer* self, - GIcon* value ) -{ - g_return_if_fail( FOOBAR_IS_QUICK_ANSWER( self ) ); - - if ( self->icon != value ) - { - g_clear_object( &self->icon ); - self->icon = value ? g_object_ref( value ) : NULL; - g_object_notify_by_pspec( G_OBJECT( self ), answer_props[ANSWER_PROP_ICON] ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Service Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the quick answer service. -// -void foobar_quick_answer_service_class_init( FoobarQuickAnswerServiceClass* klass ) -{ - (void)klass; -} - -// -// Instance initialization for the quick answer service. -// -void foobar_quick_answer_service_init( FoobarQuickAnswerService* self ) -{ - (void)self; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new quick answer service instance. -// -FoobarQuickAnswerService* foobar_quick_answer_service_new( void ) -{ - return g_object_new( FOOBAR_TYPE_QUICK_ANSWER_SERVICE, NULL ); -} - -// -// Query the service for a quick answer. -// -FoobarQuickAnswer* foobar_quick_answer_service_query( - FoobarQuickAnswerService* self, - gchar const* query ) -{ - (void)self; - - FoobarQuickAnswer* result = foobar_quick_answer_service_query_math( query ); - if ( result ) { return result; } - - return NULL; -} - -// -// Try to parse the query as a mathematical expression and evaluate it. -// -FoobarQuickAnswer* foobar_quick_answer_service_query_math( gchar const* query ) -{ - FoobarQuickAnswer* result = NULL; - - gsize token_count; - g_autofree FoobarMathToken* tokens = foobar_math_lex( query, strlen( query ), &token_count ); - if ( tokens ) - { - FoobarMathExpression* expr = foobar_math_parse( tokens, token_count ); - if ( expr ) - { - // foobar_math_expression_print( expr, 0 ); - - FoobarMathValue val = { 0 }; - if ( foobar_math_evaluate( expr, &val ) ) - { - g_autofree gchar* value = foobar_math_value_to_string( val ); - g_autofree gchar* title = g_strdup_printf( "= %s", value ); - g_autoptr( GIcon ) icon = g_themed_icon_new( "fluent-calculator-symbolic" ); - result = foobar_quick_answer_new( ); - foobar_quick_answer_set_value( result, value ); - foobar_quick_answer_set_title( result, title ); - foobar_quick_answer_set_icon( result, icon ); - foobar_math_value_free( val ); - } - - foobar_math_expression_free( expr ); - } - } - - return result; -} diff --git a/src/services/quick-answer-service.h b/src/services/quick-answer-service.h deleted file mode 100644 index adb0701..0000000 --- a/src/services/quick-answer-service.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_QUICK_ANSWER foobar_quick_answer_get_type( ) -#define FOOBAR_TYPE_QUICK_ANSWER_SERVICE foobar_quick_answer_service_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarQuickAnswer, foobar_quick_answer, FOOBAR, QUICK_ANSWER, GObject ) - -gchar const* foobar_quick_answer_get_value( FoobarQuickAnswer* self ); - -G_DECLARE_FINAL_TYPE( FoobarQuickAnswerService, foobar_quick_answer_service, FOOBAR, QUICK_ANSWER_SERVICE, GObject ) - -FoobarQuickAnswerService* foobar_quick_answer_service_new ( void ); -FoobarQuickAnswer* foobar_quick_answer_service_query ( FoobarQuickAnswerService* self, - gchar const* query ); - -G_END_DECLS diff --git a/src/services/quick-answers/math-lexer.c b/src/services/quick-answers/math-lexer.c deleted file mode 100644 index 7fbe877..0000000 --- a/src/services/quick-answers/math-lexer.c +++ /dev/null @@ -1,248 +0,0 @@ -#include "services/quick-answers/math.h" - -// -// FoobarMathToken: -// -// A token in a mathematical expression. The token contents are left as-is and need not be null-terminated. For example, -// "sin(pi) - 3" would be represented as: -// - IDENTIFIER: "sin" -// - PAREN_OPEN: "(" -// - IDENTIFIER: "pi" -// - PAREN_CLOSE: ")" -// - MINUS: "-" -// - NUMBER: "3" -// - -// -// Lexer: -// -// The lexer context/state which is passed around. -// - -typedef struct _Lexer Lexer; - -struct _Lexer -{ - GArray* result; - gchar const* input; - gsize input_length; - gsize position; - gsize token_start; -}; - -static gboolean char_is_whitespace( char c ); -static gboolean char_is_digit ( char c ); -static gboolean char_is_identifier( char c ); - -static char lexer_peek ( Lexer const* ctx, - gsize offset ); -static char lexer_pop ( Lexer* ctx ); -static void lexer_begin_token ( Lexer* ctx ); -static void lexer_commit_token ( Lexer* ctx, - FoobarMathTokenType type ); -static void lexer_commit_single( Lexer* ctx, - FoobarMathTokenType type ); - -// --------------------------------------------------------------------------------------------------------------------- -// Lexing -// --------------------------------------------------------------------------------------------------------------------- - -// -// Divide an input string into a list of tokens. -// -// On success, this will return a newly allocated array of tokens which should be freed using g_free. The number of -// elements will be written into out_count. -// -// On error, this will return NULL. -// -FoobarMathToken* foobar_math_lex( - gchar const* input, - gsize input_length, - gsize* out_count ) -{ - g_autoptr( GArray ) result = g_array_new( FALSE, TRUE, sizeof( FoobarMathToken ) ); - - Lexer ctx = { 0 }; - ctx.result = result; - ctx.input = input; - ctx.input_length = input_length; - - while ( ctx.position < input_length ) - { - if ( char_is_whitespace( lexer_peek( &ctx, 0 ) ) ) - { - // Skip whitespace. - - lexer_pop( &ctx ); - } - else if ( char_is_digit( lexer_peek( &ctx, 0 ) ) ) - { - // Process numbers as a whole, don't allow whitespace inbetween. - - lexer_begin_token( &ctx ); - while ( char_is_digit( lexer_peek( &ctx, 0 ) ) ) - { - lexer_pop( &ctx ); - } - - if ( lexer_peek( &ctx, 0 ) == '.' && char_is_digit( lexer_peek( &ctx, 1 ) ) ) - { - lexer_pop( &ctx ); - while ( char_is_digit( lexer_peek( &ctx, 0 ) ) ) - { - lexer_pop( &ctx ); - } - } - - lexer_commit_token( &ctx, FOOBAR_MATH_TOKEN_NUMBER ); - } - else if ( char_is_identifier( lexer_peek( &ctx, 0 ) ) ) - { - // Don't validate identifiers, leave that to the parser. - - lexer_begin_token( &ctx ); - while ( char_is_identifier( lexer_peek( &ctx, 0 ) ) ) - { - lexer_pop( &ctx ); - } - lexer_commit_token( &ctx, FOOBAR_MATH_TOKEN_IDENTIFIER ); - } - else if ( lexer_peek( &ctx, 0 ) == '(' ) - { - lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PAREN_OPEN ); - } - else if ( lexer_peek( &ctx, 0 ) == ')' ) - { - lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PAREN_CLOSE ); - } - else if ( lexer_peek( &ctx, 0 ) == '+' ) - { - lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_PLUS ); - } - else if ( lexer_peek( &ctx, 0 ) == '-' ) - { - lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_MINUS ); - } - else if ( lexer_peek( &ctx, 0 ) == '*' ) - { - lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_MUL ); - } - else if ( lexer_peek( &ctx, 0 ) == '/' ) - { - lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_DIV ); - } - else if ( lexer_peek( &ctx, 0 ) == '^' ) - { - lexer_commit_single( &ctx, FOOBAR_MATH_TOKEN_POW ); - } - else - { - // Unknown character. - - return NULL; - } - } - - return g_array_steal( result, out_count ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Character Classification -// --------------------------------------------------------------------------------------------------------------------- - -// -// Check if a character is a whitespace symbol (space, tab, carriage return). -// -gboolean char_is_whitespace( char c ) -{ - return c == ' ' || c == '\t' || c == '\r'; -} - -// -// Check if a character is a decimal digit. -// -gboolean char_is_digit( char c ) -{ - return c >= '0' && c <= '9'; -} - -// -// Check if a character belongs to an identifier (like "sin", "pi", etc.). -// -gboolean char_is_identifier( char c ) -{ - // Identifiers are all lowercase ASCII. - - return c >= 'a' && c <= 'z'; -} - -// --------------------------------------------------------------------------------------------------------------------- -// State Helpers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Peek at the current character without consuming it. -// -// If this is the end of the input string, return 0. -// -char lexer_peek( - Lexer const* ctx, - gsize offset ) -{ - gsize position = ctx->position + offset; - if ( position >= ctx->input_length ) { return 0; } - - return ctx->input[position]; -} - -// -// Consume the current character, moving forward to the next one. -// -// If this is the end of the input string, return 0. -// -char lexer_pop( Lexer* ctx ) -{ - if ( ctx->position >= ctx->input_length ) { return 0; } - - return ctx->input[ctx->position++]; -} - -// -// Start recording a token by remembering the current position. -// -// The current character will be the first one in the token. -// -void lexer_begin_token( Lexer* ctx ) -{ - ctx->token_start = ctx->position; -} - -// -// Stop recording a token, saving the string between the call to lexer_begin_token as the specified type. -// -// The current character will not be a part of the token. -// -void lexer_commit_token( - Lexer* ctx, - FoobarMathTokenType type ) -{ - FoobarMathToken token = { 0 }; - token.data = &ctx->input[ctx->token_start]; - token.length = ctx->position - ctx->token_start; - token.type = type; - g_array_append_val( ctx->result, token ); -} - -// -// Consume the current character and record it as a token. -// -// This is equivalent to calling lexer_begin_token, lexer_pop, and lexer_commit_token. -// -void lexer_commit_single( - Lexer* ctx, - FoobarMathTokenType type ) -{ - lexer_begin_token( ctx ); - lexer_pop( ctx ); - lexer_commit_token( ctx, type ); -} diff --git a/src/services/quick-answers/math-parser.c b/src/services/quick-answers/math-parser.c deleted file mode 100644 index 17d976d..0000000 --- a/src/services/quick-answers/math-parser.c +++ /dev/null @@ -1,414 +0,0 @@ -#include "services/quick-answers/math.h" - -// -// Parser: -// -// The parser context/state which is passed around. -// - -typedef struct _Parser Parser; - -struct _Parser -{ - FoobarMathToken const* tokens; - gsize tokens_count; - gsize position; -}; - -// -// InfixPrecedence: -// -// Value indicating how strongly infix operators should bind. The higher the value, the stronger they should bind. -// - -typedef enum -{ - INFIX_PRECEDENCE_INVALID = -1, - INFIX_PRECEDENCE_NONE = 0, - INFIX_PRECEDENCE_ADDITIVE = 1, - INFIX_PRECEDENCE_MULTIPLICATIVE = 2, - INFIX_PRECEDENCE_EXPONENTS = 3, -} InfixPrecedence; - -static FoobarMathExpression* parser_process ( Parser* ctx, - FoobarMathExpression* lhs, - InfixPrecedence min_precedence ); -static FoobarMathExpression* parser_process_single ( Parser* ctx, - gboolean allow_sign ); -static FoobarMathExpression* parser_process_identifier ( Parser* ctx ); -static FoobarMathExpression* parser_process_number ( Parser* ctx ); -static FoobarMathExpression* parser_process_function ( Parser* ctx, - FoobarMathFunction function ); -static FoobarMathExpression* parser_process_parens ( Parser* ctx ); -static FoobarMathExpression* parser_process_sign ( Parser* ctx ); -static FoobarMathToken const* parser_peek ( Parser const* ctx ); -static FoobarMathToken const* parser_pop ( Parser* ctx ); -static gboolean parser_match_identifier ( FoobarMathToken const* token, - gchar const* identifier ); -static InfixPrecedence parser_operator_precedence( FoobarMathToken const* token ); -static FoobarMathOperation parser_operator ( FoobarMathToken const* token ); - -// --------------------------------------------------------------------------------------------------------------------- -// Parsing -// --------------------------------------------------------------------------------------------------------------------- - -// -// Transform a sequence of tokens into a mathematical expression. -// -// On success, this will return a newly allocated expression which should be freed using foobar_math_expression_free. -// -// On error, this will return NULL. -// -FoobarMathExpression* foobar_math_parse( - FoobarMathToken const* tokens, - gsize tokens_count ) -{ - Parser ctx = { 0 }; - ctx.tokens = tokens; - ctx.tokens_count = tokens_count; - - FoobarMathExpression* lhs = parser_process_single( &ctx, TRUE ); - if ( !lhs ) { return NULL; } - - FoobarMathExpression* expr = parser_process( &ctx, lhs, INFIX_PRECEDENCE_NONE ); - if ( !expr ) { return NULL; } - - if ( parser_peek( &ctx ) ) - { - foobar_math_expression_free( expr ); - return NULL; - } - - return expr; -} - -// -// Process tokens until the end of the current precedence scope (indicated by min_precedence), starting at an operator. -// -FoobarMathExpression* parser_process( - Parser* ctx, - FoobarMathExpression* lhs, - InfixPrecedence min_precedence ) -{ - FoobarMathToken const* op = parser_peek( ctx ); - InfixPrecedence op_precedence = parser_operator_precedence( op ); - while ( op_precedence >= min_precedence ) - { - parser_pop( ctx ); - - FoobarMathExpression* rhs = parser_process_single( ctx, FALSE ); - if ( !rhs ) - { - foobar_math_expression_free( lhs ); - return NULL; - } - - FoobarMathToken const* next_op = parser_peek( ctx ); - while ( parser_operator_precedence( next_op ) > op_precedence ) - { - rhs = parser_process( ctx, rhs, op_precedence + 1 ); - if ( !rhs ) - { - foobar_math_expression_free( lhs ); - return NULL; - } - - next_op = parser_peek( ctx ); - } - - lhs = foobar_math_expression_new_operation( parser_operator( op ), lhs, rhs ); - - op = parser_peek( ctx ); - op_precedence = parser_operator_precedence( op ); - } - - return lhs; -} - -// -// Process a single value which may be either a constant, function, number, expression enclosed in parenthesis or -// negation. -// -// If an unexpected token is encountered (i.e. an operator), this will return FALSE. -// -FoobarMathExpression* parser_process_single( - Parser* ctx, - gboolean allow_sign ) -{ - FoobarMathToken const* token = parser_peek( ctx ); - if ( !token ) { return NULL; } - - switch ( token->type ) - { - case FOOBAR_MATH_TOKEN_IDENTIFIER: - return parser_process_identifier( ctx ); - case FOOBAR_MATH_TOKEN_NUMBER: - return parser_process_number( ctx ); - case FOOBAR_MATH_TOKEN_PAREN_OPEN: - return parser_process_parens( ctx ); - case FOOBAR_MATH_TOKEN_MINUS: - case FOOBAR_MATH_TOKEN_PLUS: - if ( !allow_sign ) { return NULL; } - return parser_process_sign( ctx ); - case FOOBAR_MATH_TOKEN_PAREN_CLOSE: - case FOOBAR_MATH_TOKEN_MUL: - case FOOBAR_MATH_TOKEN_DIV: - case FOOBAR_MATH_TOKEN_POW: - return NULL; - default: - g_warn_if_reached( ); - return NULL; - } -} - -// -// Parse an identifier. -// -// If the identifier is a function, this will expected parenthesis and an expression afterwards. -// -FoobarMathExpression* parser_process_identifier( Parser* ctx ) -{ - FoobarMathToken const* token = parser_pop( ctx ); - if ( parser_match_identifier( token, "pi" ) ) - { - return foobar_math_expression_new_constant( FOOBAR_MATH_CONSTANT_PI ); - } - else if ( parser_match_identifier( token, "e" ) ) - { - return foobar_math_expression_new_constant( FOOBAR_MATH_CONSTANT_E ); - } - else if ( parser_match_identifier( token, "exp" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_EXP ); - } - else if ( parser_match_identifier( token, "sqrt" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SQRT ); - } - else if ( parser_match_identifier( token, "sin" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SIN ); - } - else if ( parser_match_identifier( token, "cos" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_COS ); - } - else if ( parser_match_identifier( token, "sec" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_SEC ); - } - else if ( parser_match_identifier( token, "csc" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_CSC ); - } - else if ( parser_match_identifier( token, "tan" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_TAN ); - } - else if ( parser_match_identifier( token, "cot" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_COT ); - } - else if ( parser_match_identifier( token, "arcsin" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCSIN ); - } - else if ( parser_match_identifier( token, "arccos" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCOS ); - } - else if ( parser_match_identifier( token, "arcsec" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCSEC ); - } - else if ( parser_match_identifier( token, "arccsc" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCSC ); - } - else if ( parser_match_identifier( token, "arctan" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCTAN ); - } - else if ( parser_match_identifier( token, "arccot" ) ) - { - return parser_process_function( ctx, FOOBAR_MATH_FUNCTION_ARCCOT ); - } - else - { - return NULL; - } -} - -// -// Process a numeric value. -// -FoobarMathExpression* parser_process_number( Parser* ctx ) -{ - FoobarMathToken const* token = parser_pop( ctx ); - FoobarMathValue value = { 0 }; - if ( !foobar_math_value_from_string( token->data, token->length, &value ) ) - { - return NULL; - } - - return foobar_math_expression_new_value( value ); -} - -// -// Process a function's parameter which should be enclosed in parenthesis. -// -FoobarMathExpression* parser_process_function( - Parser* ctx, - FoobarMathFunction function ) -{ - FoobarMathToken const* opening_paren = parser_peek( ctx ); - if ( !opening_paren || opening_paren->type != FOOBAR_MATH_TOKEN_PAREN_OPEN ) - { - return NULL; - } - - FoobarMathExpression* arg = parser_process_parens( ctx ); - if ( !arg ) { return NULL; } - - return foobar_math_expression_new_function( function, arg ); -} - -// -// Process an expression enclosed in parenthesis. -// -FoobarMathExpression* parser_process_parens( Parser* ctx ) -{ - parser_pop( ctx ); - FoobarMathExpression* lhs = parser_process_single( ctx, TRUE ); - if ( !lhs ) { return NULL; } - FoobarMathExpression* result = parser_process( ctx, lhs, INFIX_PRECEDENCE_NONE ); - if ( !result ) { return NULL; } - FoobarMathToken const* closing_paren = parser_pop( ctx ); - if ( !closing_paren || closing_paren->type != FOOBAR_MATH_TOKEN_PAREN_CLOSE ) - { - foobar_math_expression_free( result ); - return NULL; - } - return result; -} - -// -// Process a sign (- or +) which may be used for negation of an expression. -// -FoobarMathExpression* parser_process_sign( Parser* ctx ) -{ - FoobarMathToken const* token = parser_pop( ctx ); - FoobarMathExpression* result = parser_process_single( ctx, FALSE ); - if ( !result ) { return NULL; } - - if ( token->type == FOOBAR_MATH_TOKEN_MINUS ) - { - return foobar_math_expression_new_function( FOOBAR_MATH_FUNCTION_NEGATE, result ); - } - else - { - return result; - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// State Helpers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Peek at the current token without consuming it. -// -// If this is the end of the token list, return NULL. -// -FoobarMathToken const* parser_peek( Parser const* ctx ) -{ - if ( ctx->position >= ctx->tokens_count ) { return NULL; } - - return &ctx->tokens[ctx->position]; -} - -// -// Consume the current token. -// -// If this is the end of the token list, return NULL. -// -FoobarMathToken const* parser_pop( Parser* ctx ) -{ - if ( ctx->position >= ctx->tokens_count ) { return NULL; } - - return &ctx->tokens[ctx->position++]; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Match a token's string against the given identifier string. -// -// This is necessary because the token's content is not a null-terminated string. -// -gboolean parser_match_identifier( - FoobarMathToken const* token, - gchar const* identifier ) -{ - if ( strlen( identifier ) != token->length ) { return FALSE; } - - return !strncmp( token->data, identifier, token->length ); -} - -// -// Get the precedence of an infix operator token or INVALID. -// -InfixPrecedence parser_operator_precedence( FoobarMathToken const* token ) -{ - if ( token ) - { - switch ( token->type ) - { - case FOOBAR_MATH_TOKEN_PLUS: - case FOOBAR_MATH_TOKEN_MINUS: - return INFIX_PRECEDENCE_ADDITIVE; - case FOOBAR_MATH_TOKEN_MUL: - case FOOBAR_MATH_TOKEN_DIV: - return INFIX_PRECEDENCE_MULTIPLICATIVE; - case FOOBAR_MATH_TOKEN_POW: - return INFIX_PRECEDENCE_EXPONENTS; - case FOOBAR_MATH_TOKEN_IDENTIFIER: - case FOOBAR_MATH_TOKEN_NUMBER: - case FOOBAR_MATH_TOKEN_PAREN_OPEN: - case FOOBAR_MATH_TOKEN_PAREN_CLOSE: - default: - break; - } - } - - return INFIX_PRECEDENCE_INVALID; -} - -// -// Get the operation for an infix operator token. This requires the token to be valid. -// -FoobarMathOperation parser_operator( FoobarMathToken const* token ) -{ - switch ( token->type ) - { - case FOOBAR_MATH_TOKEN_PLUS: - return FOOBAR_MATH_OPERATION_ADD; - case FOOBAR_MATH_TOKEN_MINUS: - return FOOBAR_MATH_OPERATION_SUB; - case FOOBAR_MATH_TOKEN_MUL: - return FOOBAR_MATH_OPERATION_MUL; - case FOOBAR_MATH_TOKEN_DIV: - return FOOBAR_MATH_OPERATION_DIV; - case FOOBAR_MATH_TOKEN_POW: - return FOOBAR_MATH_OPERATION_POW; - case FOOBAR_MATH_TOKEN_IDENTIFIER: - case FOOBAR_MATH_TOKEN_NUMBER: - case FOOBAR_MATH_TOKEN_PAREN_OPEN: - case FOOBAR_MATH_TOKEN_PAREN_CLOSE: - default: - g_warn_if_reached( ); - return 0; - } -} diff --git a/src/services/quick-answers/math.c b/src/services/quick-answers/math.c deleted file mode 100644 index 69219e2..0000000 --- a/src/services/quick-answers/math.c +++ /dev/null @@ -1,851 +0,0 @@ -#define _GNU_SOURCE - -#include "services/quick-answers/math.h" -#include - -// -// FoobarMathExpression: -// -// A fully parsed mathematical expression which can be evaluated, for example "sin(pi) - 3" would be represented as: -// - Operation: SUB -// - Function: SIN -// - Constant: PI -// - Value: 3 -// - -struct _FoobarMathExpression -{ - FoobarMathExpressionType type; - - union - { - struct - { - FoobarMathValue v; - } value; - - struct - { - FoobarMathFunction f; - FoobarMathExpression* input; - } function; - - struct - { - FoobarMathConstant c; - } constant; - - struct - { - FoobarMathOperation o; - FoobarMathExpression* lhs; - FoobarMathExpression* rhs; - } operation; - }; -}; - -// -// FoobarMathValue: -// -// Representation of a value. The goal of this memory structure is to store the value as an integer for as long as -// possible. Because of this, there is a "decimal_places" field, so the actual value of an integer is -// $v * 10^(-decimal_places)$. -// -// If absolutely necessary (for example, because we use a function like sin), the value is converted to a floating-point -// value. -// -// Note that we use GMP to store the integer value to allow for arbitrarily large values. Because of this, the structure -// should always be freed using foobar_math_value_free. -// - -// --------------------------------------------------------------------------------------------------------------------- -// Expressions -// --------------------------------------------------------------------------------------------------------------------- - -// -// Helper to allocate a new FoobarMathExpression representing a value. -// -// Ownership of "value" is transferred to the new expression. -// -FoobarMathExpression* foobar_math_expression_new_value( FoobarMathValue value ) -{ - FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); - res->type = FOOBAR_MATH_EXPRESSION_VALUE; - res->value.v = value; - return res; -} - -// -// Helper to allocate a new FoobarMathExpression representing a function to be evaluated with another expression as its -// parameter. -// -// Ownership of "input" is transferred to the new expression. -// -FoobarMathExpression* foobar_math_expression_new_function( - FoobarMathFunction function, - FoobarMathExpression* input ) -{ - FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); - res->type = FOOBAR_MATH_EXPRESSION_FUNCTION; - res->function.f = function; - res->function.input = input; - return res; -} - -// -// Helper to allocate a new FoobarMathExpression representing a known constant. -// -FoobarMathExpression* foobar_math_expression_new_constant( FoobarMathConstant constant ) -{ - FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); - res->type = FOOBAR_MATH_EXPRESSION_CONSTANT; - res->constant.c = constant; - return res; -} - -// -// Helper to allocate a new FoobarMathExpression representing an operation to be evaluated with two expressions as its -// parameters. -// -// Ownership of "lhs" and "rhs" is transferred to the new expression. -// -FoobarMathExpression* foobar_math_expression_new_operation( - FoobarMathOperation operation, - FoobarMathExpression* lhs, - FoobarMathExpression* rhs ) -{ - FoobarMathExpression* res = g_new0( FoobarMathExpression, 1 ); - res->type = FOOBAR_MATH_EXPRESSION_OPERATION; - res->operation.o = operation; - res->operation.lhs = lhs; - res->operation.rhs = rhs; - return res; -} - -// -// Release the memory associated with a mathematical expression, along with the expressions it owns. -// -void foobar_math_expression_free( FoobarMathExpression* expression ) -{ - if ( expression ) - { - switch ( expression->type ) - { - case FOOBAR_MATH_EXPRESSION_VALUE: - foobar_math_value_free( expression->value.v ); - break; - case FOOBAR_MATH_EXPRESSION_FUNCTION: - foobar_math_expression_free( expression->function.input ); - break; - case FOOBAR_MATH_EXPRESSION_CONSTANT: - break; - case FOOBAR_MATH_EXPRESSION_OPERATION: - foobar_math_expression_free( expression->operation.lhs ); - foobar_math_expression_free( expression->operation.rhs ); - break; - default: - g_warn_if_reached( ); - break; - } - - g_free( expression ); - } -} - -// -// Print a tree representation of a mathematical expression. -// -// This is mainly useful for debugging. "indentation" describes the current indentation level for the recursive call -// (should be 0 initially). -// -void foobar_math_expression_print( - FoobarMathExpression const* expr, - gint indentation ) -{ - for (gint i = 0; i < indentation; ++i) { g_print("| "); } - switch ( expr->type ) - { - case FOOBAR_MATH_EXPRESSION_VALUE: - { - g_autofree gchar* val = foobar_math_value_to_string( expr->value.v ); - g_print( "%s\n", val ); - break; - } - case FOOBAR_MATH_EXPRESSION_FUNCTION: - switch ( expr->function.f ) - { - case FOOBAR_MATH_FUNCTION_NEGATE: - g_print( "-\n" ); - break; - case FOOBAR_MATH_FUNCTION_SIN: - g_print( "sin\n" ); - break; - case FOOBAR_MATH_FUNCTION_COS: - g_print( "cos\n" ); - break; - case FOOBAR_MATH_FUNCTION_SEC: - g_print( "sec\n" ); - break; - case FOOBAR_MATH_FUNCTION_CSC: - g_print( "csc\n" ); - break; - case FOOBAR_MATH_FUNCTION_TAN: - g_print( "tan\n" ); - break; - case FOOBAR_MATH_FUNCTION_COT: - g_print( "cot\n" ); - break; - case FOOBAR_MATH_FUNCTION_ARCSIN: - g_print( "arcsin\n" ); - break; - case FOOBAR_MATH_FUNCTION_ARCCOS: - g_print( "arccos\n" ); - break; - case FOOBAR_MATH_FUNCTION_ARCSEC: - g_print( "arcsec\n" ); - break; - case FOOBAR_MATH_FUNCTION_ARCCSC: - g_print( "arccsc\n" ); - break; - case FOOBAR_MATH_FUNCTION_ARCTAN: - g_print( "arctan\n" ); - break; - case FOOBAR_MATH_FUNCTION_ARCCOT: - g_print( "arccot\n" ); - break; - case FOOBAR_MATH_FUNCTION_EXP: - g_print( "exp\n" ); - break; - case FOOBAR_MATH_FUNCTION_SQRT: - g_print( "sqrt\n" ); - break; - default: - g_warn_if_reached( ); - break; - } - foobar_math_expression_print( expr->function.input, indentation + 1 ); - break; - case FOOBAR_MATH_EXPRESSION_CONSTANT: - switch ( expr->constant.c ) - { - case FOOBAR_MATH_CONSTANT_PI: - g_print( "pi\n" ); - break; - case FOOBAR_MATH_CONSTANT_E: - g_print( "e\n" ); - break; - default: - g_warn_if_reached( ); - break; - } - break; - case FOOBAR_MATH_EXPRESSION_OPERATION: - switch ( expr->operation.o ) - { - case FOOBAR_MATH_OPERATION_ADD: - g_print( "+\n" ); - break; - case FOOBAR_MATH_OPERATION_SUB: - g_print( "-\n" ); - break; - case FOOBAR_MATH_OPERATION_MUL: - g_print( "*\n" ); - break; - case FOOBAR_MATH_OPERATION_DIV: - g_print( "/\n" ); - break; - case FOOBAR_MATH_OPERATION_POW: - g_print( "^\n" ); - break; - default: - g_warn_if_reached( ); - break; - } - foobar_math_expression_print( expr->operation.lhs, indentation + 1 ); - foobar_math_expression_print( expr->operation.rhs, indentation + 1 ); - break; - default: - g_warn_if_reached( ); - break; - } -} - -// -// Evaluate an expression, producing a value. -// -// On success (indicated by the return value TRUE), the value should be freed using foobar_math_value_free. -// -gboolean foobar_math_evaluate( - FoobarMathExpression const* expr, - FoobarMathValue* out_value ) -{ - switch ( expr->type ) - { - case FOOBAR_MATH_EXPRESSION_VALUE: - foobar_math_value_copy( expr->value.v, out_value ); - return TRUE; - case FOOBAR_MATH_EXPRESSION_CONSTANT: - switch ( expr->constant.c ) - { - case FOOBAR_MATH_CONSTANT_PI: - foobar_math_value_from_float( M_PIl, out_value ); - return TRUE; - case FOOBAR_MATH_CONSTANT_E: - foobar_math_value_from_float( M_El, out_value ); - return TRUE; - default: - g_warn_if_reached( ); - return FALSE; - } - case FOOBAR_MATH_EXPRESSION_FUNCTION: - { - FoobarMathValue input = { 0 }; - if ( !foobar_math_evaluate( expr->function.input, &input ) ) { return FALSE; } - - if ( expr->function.f == FOOBAR_MATH_FUNCTION_NEGATE ) - { - *out_value = input; - foobar_math_value_negate( out_value ); - return TRUE; - } - - long double input_float = foobar_math_value_to_float( input ); - foobar_math_value_free( input ); - - long double result = NAN; - switch ( expr->function.f ) - { - case FOOBAR_MATH_FUNCTION_SIN: - result = sinl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_COS: - result = cosl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_SEC: - result = 1.L / cosl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_CSC: - result = 1.L / sinl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_TAN: - result = tanl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_COT: - result = 1.L / tanl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_ARCSIN: - result = asinl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_ARCCOS: - result = acosl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_ARCSEC: - result = acosl( 1.L / input_float ); - break; - case FOOBAR_MATH_FUNCTION_ARCCSC: - result = asinl( 1.L / input_float ); - break; - case FOOBAR_MATH_FUNCTION_ARCTAN: - result = atanl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_ARCCOT: - result = atanl( 1.L / input_float ); - break; - case FOOBAR_MATH_FUNCTION_EXP: - result = expl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_SQRT: - result = sqrtl( input_float ); - break; - case FOOBAR_MATH_FUNCTION_NEGATE: - default: - g_warn_if_reached( ); - return FALSE; - } - - if ( isnanl( result ) || isinfl( result ) ) { return FALSE; } - - foobar_math_value_from_float( result, out_value ); - return TRUE; - } - case FOOBAR_MATH_EXPRESSION_OPERATION: - { - FoobarMathValue lhs = { 0 }; - if ( !foobar_math_evaluate( expr->operation.lhs, &lhs ) ) { return FALSE; } - FoobarMathValue rhs = { 0 }; - if ( !foobar_math_evaluate( expr->operation.rhs, &rhs ) ) - { - foobar_math_value_free( lhs ); - return FALSE; - } - - gboolean success = FALSE; - switch ( expr->operation.o ) - { - case FOOBAR_MATH_OPERATION_ADD: - success = foobar_math_value_add( &lhs, &rhs, out_value ); - break; - case FOOBAR_MATH_OPERATION_SUB: - success = foobar_math_value_sub( &lhs, &rhs, out_value ); - break; - case FOOBAR_MATH_OPERATION_MUL: - success = foobar_math_value_mul( &lhs, &rhs, out_value ); - break; - case FOOBAR_MATH_OPERATION_DIV: - success = foobar_math_value_div( &lhs, &rhs, out_value ); - break; - case FOOBAR_MATH_OPERATION_POW: - success = foobar_math_value_pow( &lhs, &rhs, out_value ); - break; - default: - g_warn_if_reached( ); - break; - } - - foobar_math_value_free( lhs ); - foobar_math_value_free( rhs ); - - return success; - } - default: - g_warn_if_reached( ); - return FALSE; - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Values -// --------------------------------------------------------------------------------------------------------------------- - -// -// Initialize a new integer value to zero. -// -// After this, the structure should be freed using foobar_math_value_free. -// -void foobar_math_value_new_int( FoobarMathValue* out_value ) -{ - out_value->type = FOOBAR_MATH_VALUE_INT; - mpz_init( out_value->int_value.v ); - out_value->int_value.decimal_places = 0; -} - -// -// Initialize a new value from its floating point representation. -// -// Although technically not necessary, you should still call foobar_math_value_free on the returned value. -// -void foobar_math_value_from_float( - long double value, - FoobarMathValue* out_value ) -{ - out_value->type = FOOBAR_MATH_VALUE_FLOAT; - out_value->float_value.v = value; -} - -// -// Parse a value from its string representation. This will always return an integer representation. -// -// On success (return value TRUE), the structure should be freed using foobar_math_value_free. -// -gboolean foobar_math_value_from_string( - gchar const* input, - gsize input_length, - FoobarMathValue* out_value ) -{ - g_autofree gchar* without_sep = g_new0( gchar, input_length + 1 ); - gsize sep_pos = input_length - 1; - gsize j = 0; - for ( gsize i = 0; i < input_length; ++i ) - { - if ( input[i] == '.' ) - { - sep_pos = i; - } - else - { - without_sep[j++] = input[i]; - } - } - - foobar_math_value_new_int( out_value ); - out_value->int_value.decimal_places = input_length - 1 - sep_pos; - if ( mpz_init_set_str( out_value->int_value.v, without_sep, 10 ) == -1 ) - { - mpz_clear( out_value->int_value.v ); - return FALSE; - } - - return TRUE; -} - -// -// Create a copy of a value, allocating necessary memory. -// -// After this, the structure should be freed using foobar_math_value_free. -// -void foobar_math_value_copy( - FoobarMathValue value, - FoobarMathValue* out_value ) -{ - *out_value = value; - - switch ( value.type ) - { - case FOOBAR_MATH_VALUE_INT: - mpz_init_set( out_value->int_value.v, value.int_value.v ); - break; - case FOOBAR_MATH_VALUE_FLOAT: - break; - default: - g_warn_if_reached( ); - break; - } -} - -// -// Release memory associated with a value. -// -// This is necessary because the integer representation is stored using GMP, which can store arbitrarily large values. -// -void foobar_math_value_free( FoobarMathValue value ) -{ - switch ( value.type ) - { - case FOOBAR_MATH_VALUE_INT: - mpz_clear( value.int_value.v ); - break; - case FOOBAR_MATH_VALUE_FLOAT: - break; - default: - g_warn_if_reached( ); - break; - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Operations -// --------------------------------------------------------------------------------------------------------------------- - -// -// Operation equivalent to "value *= -1". -// -void foobar_math_value_negate( FoobarMathValue* value ) -{ - switch ( value->type ) - { - case FOOBAR_MATH_VALUE_INT: - mpz_neg( value->int_value.v, value->int_value.v ); - break; - case FOOBAR_MATH_VALUE_FLOAT: - value->float_value.v = -value->float_value.v; - break; - default: - g_warn_if_reached( ); - break; - } -} - -// -// Operation setting "out_value = lhs + rhs". -// -// After this, the result value should be freed using foobar_math_value_free. -// -gboolean foobar_math_value_add( - FoobarMathValue* lhs, - FoobarMathValue* rhs, - FoobarMathValue* out_value ) -{ - if ( foobar_math_value_unify_integers( lhs, rhs ) ) - { - foobar_math_value_new_int( out_value ); - out_value->int_value.decimal_places = lhs->int_value.decimal_places; - mpz_add( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v ); - return TRUE; - } - - long double lhs_float = foobar_math_value_to_float( *lhs ); - long double rhs_float = foobar_math_value_to_float( *rhs ); - foobar_math_value_from_float( lhs_float + rhs_float, out_value ); - return TRUE; -} - -// -// Operation setting "out_value = lhs - rhs". -// -// After this, the result value should be freed using foobar_math_value_free. -// -gboolean foobar_math_value_sub( - FoobarMathValue* lhs, - FoobarMathValue* rhs, - FoobarMathValue* out_value ) -{ - if ( foobar_math_value_unify_integers( lhs, rhs ) ) - { - foobar_math_value_new_int( out_value ); - out_value->int_value.decimal_places = lhs->int_value.decimal_places; - mpz_sub( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v ); - return TRUE; - } - - long double lhs_float = foobar_math_value_to_float( *lhs ); - long double rhs_float = foobar_math_value_to_float( *rhs ); - foobar_math_value_from_float( lhs_float - rhs_float, out_value ); - return TRUE; -} - -// -// Operation setting "out_value = lhs * rhs". -// -// After this, the result value should be freed using foobar_math_value_free. -// -gboolean foobar_math_value_mul( - FoobarMathValue* lhs, - FoobarMathValue* rhs, - FoobarMathValue* out_value ) -{ - if ( lhs->type == FOOBAR_MATH_VALUE_INT && rhs->type == FOOBAR_MATH_VALUE_INT ) - { - foobar_math_value_new_int( out_value ); - out_value->int_value.decimal_places = lhs->int_value.decimal_places + rhs->int_value.decimal_places; - mpz_mul( out_value->int_value.v, lhs->int_value.v, rhs->int_value.v ); - return TRUE; - } - - long double lhs_float = foobar_math_value_to_float( *lhs ); - long double rhs_float = foobar_math_value_to_float( *rhs ); - foobar_math_value_from_float( lhs_float * rhs_float, out_value ); - return TRUE; -} - -// -// Operation setting "out_value = lhs / rhs". -// -// After this, the result value should be freed using foobar_math_value_free. -// -gboolean foobar_math_value_div( - FoobarMathValue* lhs, - FoobarMathValue* rhs, - FoobarMathValue* out_value ) -{ - if ( lhs->type == FOOBAR_MATH_VALUE_INT && rhs->type == FOOBAR_MATH_VALUE_INT ) - { - if ( mpz_sgn( rhs->int_value.v ) == 0 ) { return FALSE; } - - if ( rhs->int_value.decimal_places > lhs->int_value.decimal_places ) - { - foobar_math_value_unify_integers( lhs, rhs ); - } - - foobar_math_value_new_int( out_value ); - out_value->int_value.decimal_places = lhs->int_value.decimal_places - rhs->int_value.decimal_places; - mpz_t rem; - mpz_init( rem ); - mpz_divmod( out_value->int_value.v, rem, lhs->int_value.v, rhs->int_value.v ); - gboolean result_is_int = mpz_sgn( rem ) == 0; - mpz_clear( rem ); - - if ( result_is_int ) { return TRUE; } - foobar_math_value_free( *out_value ); - } - - long double lhs_float = foobar_math_value_to_float( *lhs ); - long double rhs_float = foobar_math_value_to_float( *rhs ); - long double result = lhs_float / rhs_float; - if ( isnanl( result ) || isinfl( result ) ) { return FALSE; } - foobar_math_value_from_float( result, out_value ); - return TRUE; -} - -// -// Operation setting "out_value = lhs ^ rhs". -// -// After this, the result value should be freed using foobar_math_value_free. -// -gboolean foobar_math_value_pow( - FoobarMathValue* lhs, - FoobarMathValue* rhs, - FoobarMathValue* out_value ) -{ - if ( lhs->type == FOOBAR_MATH_VALUE_INT && - rhs->type == FOOBAR_MATH_VALUE_INT && - rhs->int_value.decimal_places == 0 && - mpz_fits_ulong_p( rhs->int_value.v ) ) - { - unsigned long exp = mpz_get_ui( rhs->int_value.v ); - foobar_math_value_new_int( out_value ); - out_value->int_value.decimal_places = lhs->int_value.decimal_places * exp; - mpz_pow_ui( out_value->int_value.v, lhs->int_value.v, exp ); - return TRUE; - } - - long double lhs_float = foobar_math_value_to_float( *lhs ); - long double rhs_float = foobar_math_value_to_float( *rhs ); - long double result = powl( lhs_float, rhs_float ); - if ( isnanl( result ) || isinfl( result ) ) { return FALSE; } - foobar_math_value_from_float( result, out_value ); - return TRUE; -} - -// -// Ensure that two integer values have the same number of decimal places. -// -// This may change the in-memory-representation of a value, but not its actual value. It only increases the precision of -// one value -// -// If either a or b is not stored as an integer, this will return FALSE. -// -gboolean foobar_math_value_unify_integers( - FoobarMathValue* a, - FoobarMathValue* b ) -{ - if ( a->type != FOOBAR_MATH_VALUE_INT || b->type != FOOBAR_MATH_VALUE_INT ) { return FALSE; } - - // Extend the less precise representation so both numbers have the same number of decimal places. - - gsize decimal_places = MAX( a->int_value.decimal_places, b->int_value.decimal_places ); - - mpz_t exp; - mpz_init( exp ); - - if ( decimal_places > a->int_value.decimal_places ) - { - mpz_ui_pow_ui( exp, 10, decimal_places - a->int_value.decimal_places ); - mpz_mul( a->int_value.v, a->int_value.v, exp ); - } - - if ( decimal_places > b->int_value.decimal_places ) - { - mpz_ui_pow_ui( exp, 10, decimal_places - b->int_value.decimal_places ); - mpz_mul( b->int_value.v, b->int_value.v, exp ); - } - - mpz_clear( exp ); - - a->int_value.decimal_places = b->int_value.decimal_places = decimal_places; - - return TRUE; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Conversions -// --------------------------------------------------------------------------------------------------------------------- - -// -// Format a value as a string. -// -// The result is a null-terminated string which should be freed using g_free. -// -gchar* foobar_math_value_to_string( FoobarMathValue value ) -{ - switch ( value.type ) - { - case FOOBAR_MATH_VALUE_INT: - { - gsize cap = mpz_sizeinbase( value.int_value.v, 10 ); - cap = MAX( cap, value.int_value.decimal_places + 1 ); // pad with zeroes if necessary - gchar* result = g_new0( gchar, cap + 3 ); // length + null terminator + sign + separator - mpz_get_str( result, 10, value.int_value.v ); - gsize len = strlen( result ); // actual size may be 1 char less (+ len now also includes sign) - gboolean has_sign = result[0] == '-'; - if ( has_sign ) - { - // add the sign back later, makes everything else easier - result += 1; - len -= 1; - } - // pad with leading zeroes if necessary - gsize padded_len = MAX( len, value.int_value.decimal_places + 1 ); - memmove( &result[padded_len - len], result, len + 1 ); - memset( result, '0', padded_len - len ); - len = padded_len; - gboolean trailing_zeros = TRUE; - gsize decimal_places = value.int_value.decimal_places; - gsize actual_decimal_places = decimal_places; - for ( gsize i = 0; i < decimal_places; ++i ) - { - if ( trailing_zeros && result[len - 1 - i] == '0' ) - { - result[len - 1 - i] = '\0'; - actual_decimal_places -= 1; - } - else - { - trailing_zeros = FALSE; - } - } - if ( actual_decimal_places > 0 ) - { - memmove( &result[len + 1 - decimal_places], &result[len - decimal_places], decimal_places + 1 ); - result[len - decimal_places] = '.'; - } - if ( has_sign ) - { - result -= 1; - len += 1; - } - return result; - } - case FOOBAR_MATH_VALUE_FLOAT: - { - gchar* result = g_strdup_printf( "%.30Lf", value.float_value.v ); - - // trim trailing zeros - size_t len = strlen( result ); - for ( gsize i = 0; i < len; ++i ) - { - if ( result[len - 1 - i] == '.' ) - { - result[len - 1 - i] = '\0'; - break; - } - else if ( result[len - 1 - i] == '0' ) - { - result[len - 1 - i] = '\0'; - } - else - { - break; - } - } - - return result; - } - default: - g_warn_if_reached( ); - return NULL; - } -} - -// -// Convert a value to its floating-point representation. -// -// This may be a lossy conversion and should only be done if absolutely necessary. -// -long double foobar_math_value_to_float( FoobarMathValue value ) -{ - switch ( value.type ) - { - case FOOBAR_MATH_VALUE_INT: - { - mpz_t exp, integer, frac; - mpz_inits( exp, integer, frac, NULL ); - mpz_ui_pow_ui( exp, 10, value.int_value.decimal_places ); - mpz_mod( frac, value.int_value.v, exp ); - mpz_div( integer, value.int_value.v, exp ); - mpf_t f_exp, f_frac; - mpf_inits( f_exp, f_frac, NULL ); - mpf_set_z( f_exp, exp ); - mpf_set_z( f_frac, frac ); - mpf_div( f_frac, f_frac, f_exp ); - long double result = mpz_get_d( integer ) + mpf_get_d( f_frac ); - mpf_clears( f_exp, f_frac, NULL ); - mpz_clears( exp, integer, frac, NULL ); - return result; - } - case FOOBAR_MATH_VALUE_FLOAT: - return value.float_value.v; - default: - g_warn_if_reached( ); - return 0; - } -} diff --git a/src/services/quick-answers/math.h b/src/services/quick-answers/math.h deleted file mode 100644 index fafa73d..0000000 --- a/src/services/quick-answers/math.h +++ /dev/null @@ -1,148 +0,0 @@ -#pragma once - -#include -#include - -G_BEGIN_DECLS - -typedef enum -{ - FOOBAR_MATH_VALUE_INT, - FOOBAR_MATH_VALUE_FLOAT, -} FoobarMathValueType; - -typedef struct _FoobarMathValue FoobarMathValue; - -struct _FoobarMathValue -{ - FoobarMathValueType type; - - union - { - struct - { - mpz_t v; - gsize decimal_places; - } int_value; - - struct - { - long double v; - } float_value; - }; -}; - -typedef enum -{ - FOOBAR_MATH_TOKEN_IDENTIFIER, - FOOBAR_MATH_TOKEN_NUMBER, - FOOBAR_MATH_TOKEN_PAREN_OPEN, - FOOBAR_MATH_TOKEN_PAREN_CLOSE, - FOOBAR_MATH_TOKEN_PLUS, - FOOBAR_MATH_TOKEN_MINUS, - FOOBAR_MATH_TOKEN_MUL, - FOOBAR_MATH_TOKEN_DIV, - FOOBAR_MATH_TOKEN_POW, -} FoobarMathTokenType; - -typedef struct _FoobarMathToken FoobarMathToken; - -struct _FoobarMathToken -{ - FoobarMathTokenType type; - gchar const* data; - gsize length; -}; - -typedef enum -{ - FOOBAR_MATH_EXPRESSION_VALUE, - FOOBAR_MATH_EXPRESSION_FUNCTION, - FOOBAR_MATH_EXPRESSION_CONSTANT, - FOOBAR_MATH_EXPRESSION_OPERATION, -} FoobarMathExpressionType; - -typedef enum -{ - FOOBAR_MATH_FUNCTION_NEGATE, - FOOBAR_MATH_FUNCTION_SIN, - FOOBAR_MATH_FUNCTION_COS, - FOOBAR_MATH_FUNCTION_SEC, - FOOBAR_MATH_FUNCTION_CSC, - FOOBAR_MATH_FUNCTION_TAN, - FOOBAR_MATH_FUNCTION_COT, - FOOBAR_MATH_FUNCTION_ARCSIN, - FOOBAR_MATH_FUNCTION_ARCCOS, - FOOBAR_MATH_FUNCTION_ARCSEC, - FOOBAR_MATH_FUNCTION_ARCCSC, - FOOBAR_MATH_FUNCTION_ARCTAN, - FOOBAR_MATH_FUNCTION_ARCCOT, - FOOBAR_MATH_FUNCTION_EXP, - FOOBAR_MATH_FUNCTION_SQRT, -} FoobarMathFunction; - -typedef enum -{ - FOOBAR_MATH_CONSTANT_PI, - FOOBAR_MATH_CONSTANT_E, -} FoobarMathConstant; - -typedef enum -{ - FOOBAR_MATH_OPERATION_ADD, - FOOBAR_MATH_OPERATION_SUB, - FOOBAR_MATH_OPERATION_MUL, - FOOBAR_MATH_OPERATION_DIV, - FOOBAR_MATH_OPERATION_POW, -} FoobarMathOperation; - -typedef struct _FoobarMathExpression FoobarMathExpression; - -FoobarMathToken* foobar_math_lex ( gchar const* input, - gsize input_length, - gsize* out_count ); -FoobarMathExpression* foobar_math_parse ( FoobarMathToken const* tokens, - gsize tokens_count ); -FoobarMathExpression* foobar_math_expression_new_value ( FoobarMathValue value ); -FoobarMathExpression* foobar_math_expression_new_function ( FoobarMathFunction function, - FoobarMathExpression* input ); -FoobarMathExpression* foobar_math_expression_new_constant ( FoobarMathConstant constant ); -FoobarMathExpression* foobar_math_expression_new_operation( FoobarMathOperation operation, - FoobarMathExpression* lhs, - FoobarMathExpression* rhs ); -void foobar_math_expression_print ( FoobarMathExpression const* expr, - gint indentation ); -gboolean foobar_math_evaluate ( FoobarMathExpression const* expr, - FoobarMathValue* out_value ); -void foobar_math_expression_free ( FoobarMathExpression* expression ); -void foobar_math_value_new_int ( FoobarMathValue* out_value ); -void foobar_math_value_from_float ( long double value, - FoobarMathValue* out_value ); -gboolean foobar_math_value_from_string ( gchar const* input, - gsize input_length, - FoobarMathValue* out_value ); -void foobar_math_value_copy ( FoobarMathValue value, - FoobarMathValue* out_value ); -void foobar_math_value_free ( FoobarMathValue value ); -void foobar_math_value_negate ( FoobarMathValue* value ); -gboolean foobar_math_value_add ( FoobarMathValue* lhs, - FoobarMathValue* rhs, - FoobarMathValue* out_value ); -gboolean foobar_math_value_sub ( FoobarMathValue* lhs, - FoobarMathValue* rhs, - FoobarMathValue* out_value ); -gboolean foobar_math_value_mul ( FoobarMathValue* lhs, - FoobarMathValue* rhs, - FoobarMathValue* out_value ); -gboolean foobar_math_value_div ( FoobarMathValue* lhs, - FoobarMathValue* rhs, - FoobarMathValue* out_value ); -gboolean foobar_math_value_pow ( FoobarMathValue* lhs, - FoobarMathValue* rhs, - FoobarMathValue* out_value ); -gboolean foobar_math_value_unify_integers ( FoobarMathValue* a, - FoobarMathValue* b ); -gchar* foobar_math_value_to_string ( FoobarMathValue value ); -long double foobar_math_value_to_float ( FoobarMathValue value ); - -G_END_DECLS diff --git a/src/services/quick-answers/math.test.c b/src/services/quick-answers/math.test.c deleted file mode 100644 index 416b576..0000000 --- a/src/services/quick-answers/math.test.c +++ /dev/null @@ -1,153 +0,0 @@ -#include "services/quick-answers/math.h" -#include - -#define SERIALIZATION_TEST( value, identifier ) \ - static void serialization_value_##identifier##_spec( void ) \ - { \ - FoobarMathValue val; \ - \ - gchar const input[] = #value; \ - mutest_expect( \ - "parse result", \ - mutest_bool_value( foobar_math_value_from_string( input, sizeof(input) - 1, &val) ), \ - mutest_to_be_true, \ - NULL ); \ - g_autofree gchar* output = foobar_math_value_to_string( val ); \ - mutest_expect( \ - "string representation", \ - mutest_string_value( output ), \ - mutest_to_be, \ - input, \ - NULL ); \ - \ - foobar_math_value_free( val ); \ - } - -SERIALIZATION_TEST( 0, 0 ) -SERIALIZATION_TEST( 1, 1 ) -SERIALIZATION_TEST( -1, neg_1 ) -SERIALIZATION_TEST( 1.2, 1_2 ) -SERIALIZATION_TEST( -1.2, neg_1_2 ) -SERIALIZATION_TEST( 1.30002, 1_30002 ) -SERIALIZATION_TEST( -1.30002, neg_1_30002 ) -SERIALIZATION_TEST( 0.2, 0_2 ) -SERIALIZATION_TEST( -0.2, neg_0_2 ) -SERIALIZATION_TEST( 0.00002, 0_00002 ) -SERIALIZATION_TEST( -0.00002, neg_0_00002 ) -SERIALIZATION_TEST( 123, 123 ) -SERIALIZATION_TEST( 123.456, 123_456 ) -SERIALIZATION_TEST( -123, neg_123 ) -SERIALIZATION_TEST( -123.456, neg_123_456 ) - -#undef SERIALIZATION_TEST - -static void serialization_suite( void ) -{ - mutest_it( "value 0", serialization_value_0_spec ); - mutest_it( "value 1", serialization_value_1_spec ); - mutest_it( "value -1", serialization_value_neg_1_spec ); - mutest_it( "value 1.2", serialization_value_1_2_spec ); - mutest_it( "value -1.2", serialization_value_neg_1_2_spec ); - mutest_it( "value 1.30002", serialization_value_1_30002_spec ); - mutest_it( "value -1.30002", serialization_value_neg_1_30002_spec ); - mutest_it( "value 0.2", serialization_value_0_2_spec ); - mutest_it( "value -0.2", serialization_value_neg_0_2_spec ); - mutest_it( "value 0.00002", serialization_value_0_00002_spec ); - mutest_it( "value -0.00002", serialization_value_neg_0_00002_spec ); - mutest_it( "value 123", serialization_value_123_spec ); - mutest_it( "value 123.456", serialization_value_123_456_spec ); - mutest_it( "value -123", serialization_value_neg_123_spec ); - mutest_it( "value -123.456", serialization_value_neg_123_456_spec ); -} -#define OPERATION_TEST( op, value_a, value_b, value_res, identifier ) \ - static void operation_##identifier##_spec( void ) \ - { \ - FoobarMathValue a, b, res; \ - \ - gchar const input_a[] = #value_a; \ - gchar const input_b[] = #value_b; \ - mutest_expect( \ - "input a parsed", \ - mutest_bool_value( foobar_math_value_from_string( input_a, sizeof(input_a) - 1, &a) ), \ - mutest_to_be_true, \ - NULL ); \ - mutest_expect( \ - "input b parsed", \ - mutest_bool_value( foobar_math_value_from_string( input_b, sizeof(input_b) - 1, &b) ), \ - mutest_to_be_true, \ - NULL ); \ - foobar_math_value_##op( &a, &b, &res ); \ - g_autofree gchar* output = foobar_math_value_to_string( res ); \ - mutest_expect( \ - "result", \ - mutest_string_value( output ), \ - mutest_to_be, \ - #value_res, \ - NULL ); \ - \ - foobar_math_value_free( a ); \ - foobar_math_value_free( b ); \ - foobar_math_value_free( res ); \ - } - -OPERATION_TEST( add, 123, 456, 579, 123_plus_456) -OPERATION_TEST( add, 123.456, 11, 134.456, 123_456_plus_11) -OPERATION_TEST( add, 11, 123.456, 134.456, 11_plus_123_456) -OPERATION_TEST( sub, 123, 456, -333, 123_minus_456) -OPERATION_TEST( sub, 123.456, 11, 112.456, 123_456_minus_11) -OPERATION_TEST( sub, 11, 123.456, -112.456, 11_minus_123_456) -OPERATION_TEST( mul, 123, 3, 369, 123_times_3 ) -OPERATION_TEST( mul, 12, 0.05, 0.6, 12_times_0_05 ) -OPERATION_TEST( mul, 0.05, 12, 0.6, 0_05_times_12 ) -OPERATION_TEST( div, 369, 3, 123, 369_div_3 ) -OPERATION_TEST( div, 369, 2, 184.5, 369_div_2 ) -OPERATION_TEST( div, 12, 0.05, 240, 12_div_0_05 ) -OPERATION_TEST( div, 0.08, 4, 0.02, 0_08_div_4 ) -OPERATION_TEST( pow, 2, 3, 8, 2_pow_3 ) -OPERATION_TEST( pow, 4, 0.5, 2, 4_pow_0_5 ) - -#undef OPERATION_TEST - -static void addition_suite( void ) -{ - mutest_it( "calculate 123 + 456", operation_123_plus_456_spec ); - mutest_it( "calculate 123.456 + 11", operation_123_456_plus_11_spec ); - mutest_it( "calculate 11 + 123.456", operation_11_plus_123_456_spec ); -} - -static void subtraction_suite( void ) -{ - mutest_it( "calculate 123 - 456", operation_123_minus_456_spec ); - mutest_it( "calculate 123.456 - 11", operation_123_456_minus_11_spec ); - mutest_it( "calculate 11 - 123.456", operation_11_minus_123_456_spec ); -} - -static void multiplication_suite( void ) -{ - mutest_it( "calculate 123 * 3", operation_123_times_3_spec ); - mutest_it( "calculate 12 * 0.05", operation_12_times_0_05_spec ); - mutest_it( "calculate 0.05 * 12", operation_0_05_times_12_spec ); -} - -static void division_suite( void ) -{ - mutest_it( "calculate 369 / 3", operation_369_div_3_spec ); - mutest_it( "calculate 369 / 2", operation_369_div_2_spec ); - mutest_it( "calculate 12 / 0.05", operation_12_div_0_05_spec ); - mutest_it( "calculate 0.08 / 4", operation_0_08_div_4_spec ); -} - -static void power_suite( void ) -{ - mutest_it( "calculate 2 ^ 3", operation_2_pow_3_spec ); - mutest_it( "calculate 4 ^ 0.5", operation_4_pow_0_5_spec ); -} - -MUTEST_MAIN( - mutest_describe( "Serialization", serialization_suite ); - mutest_describe( "Addition", addition_suite ); - mutest_describe( "Subtraction", subtraction_suite ); - mutest_describe( "Multiplication", multiplication_suite ); - mutest_describe( "Division", division_suite ); - mutest_describe( "Power", power_suite ); -) diff --git a/src/services/quick-answers/meson.build b/src/services/quick-answers/meson.build deleted file mode 100644 index ae468c2..0000000 --- a/src/services/quick-answers/meson.build +++ /dev/null @@ -1,9 +0,0 @@ -foobar_sources += files( - 'math-lexer.c', - 'math-parser.c', - 'math.c', -) - -foobar_tests += { - 'math': files('math.test.c'), -} diff --git a/src/services/workspace-service.c b/src/services/workspace-service.c deleted file mode 100644 index 75ea06a..0000000 --- a/src/services/workspace-service.c +++ /dev/null @@ -1,1386 +0,0 @@ -#include "services/workspace-service.h" -#include -#include -#include - -typedef struct _EventData EventData; -typedef struct _EventHandler EventHandler; - -// -// FoobarWorkspaceFlags: -// -// Some information about the type/state of a workspace. -// - -G_DEFINE_FLAGS_TYPE( - FoobarWorkspaceFlags, - foobar_workspace_flags, - G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_NONE, "none" ), - G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_ACTIVE, "active" ), - G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_VISIBLE, "visible" ), - G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_SPECIAL, "special" ), - G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_PERSISTENT, "persistent" ), - G_DEFINE_ENUM_VALUE( FOOBAR_WORKSPACE_FLAGS_URGENT, "urgent" ) ) - -// -// FoobarWorkspace: -// -// Representation of a workspace. -// - -struct _FoobarWorkspace -{ - GObject parent_instance; - FoobarWorkspaceService* service; - gint64 id; - gchar* name; - gchar* monitor; - FoobarWorkspaceFlags flags; -}; - -enum -{ - WORKSPACE_PROP_ID = 1, - WORKSPACE_PROP_NAME, - WORKSPACE_PROP_MONITOR, - WORKSPACE_PROP_FLAGS, - N_WORKSPACE_PROPS, -}; - -static GParamSpec* workspace_props[N_WORKSPACE_PROPS] = { 0 }; - -static void foobar_workspace_class_init ( FoobarWorkspaceClass* klass ); -static void foobar_workspace_init ( FoobarWorkspace* self ); -static void foobar_workspace_get_property( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_workspace_finalize ( GObject* object ); -static FoobarWorkspace* foobar_workspace_new ( FoobarWorkspaceService* service ); -static void foobar_workspace_set_id ( FoobarWorkspace* self, - gint64 value ); -static void foobar_workspace_set_name ( FoobarWorkspace* self, - gchar const* value ); -static void foobar_workspace_set_monitor ( FoobarWorkspace* self, - gchar const* value ); -static void foobar_workspace_set_flags ( FoobarWorkspace* self, - FoobarWorkspaceFlags value ); - -G_DEFINE_FINAL_TYPE( FoobarWorkspace, foobar_workspace, G_TYPE_OBJECT ) - -// -// FoobarWorkspaceService: -// -// Service monitoring the workspaces provided by the window manager. This is implemented by communicating directly with -// Hyprland through its IPC socket. -// -// Based on the waybar implementation: -// https://github.com/Alexays/Waybar/blob/master/src/modules/hyprland/workspaces.cpp -// - -struct _FoobarWorkspaceService -{ - GObject parent_instance; - GListStore* workspaces; - GtkSortListModel* sorted_workspaces; - GCancellable* event_cancellable; - GThread* event_thread; - gchar* rx_path; - gchar* tx_path; -}; - -enum -{ - PROP_WORKSPACES = 1, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -enum -{ - SIGNAL_MONITOR_CONFIGURATION_CHANGED, - N_SIGNALS, -}; -static unsigned signals[N_SIGNALS] = { 0 }; - -static void foobar_workspace_service_class_init ( FoobarWorkspaceServiceClass* klass ); -static void foobar_workspace_service_init ( FoobarWorkspaceService* self ); -static void foobar_workspace_service_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_workspace_service_dispose ( GObject* object ); -static void foobar_workspace_service_finalize ( GObject* object ); -static gboolean foobar_workspace_service_handle_workspace_created ( EventData* data ); -static gboolean foobar_workspace_service_handle_workspace_destroyed ( EventData* data ); -static gboolean foobar_workspace_service_handle_workspace_moved ( EventData* data ); -static gboolean foobar_workspace_service_handle_workspace_renamed ( EventData* data ); -static gboolean foobar_workspace_service_handle_workspace_activated ( EventData* data ); -static gboolean foobar_workspace_service_handle_special_workspace_activated( EventData* data ); -static gboolean foobar_workspace_service_handle_monitor_focused ( EventData* data ); -static gboolean foobar_workspace_service_handle_window_urgent ( EventData* data ); -static gboolean foobar_workspace_service_handle_config_reloaded ( EventData* data ); -static gpointer foobar_workspace_service_event_thread_func ( gpointer userdata ); -static void foobar_workspace_service_dispatch_event ( FoobarWorkspaceService* self, - gchar* message ); -static gchar* foobar_workspace_service_send_request ( FoobarWorkspaceService* self, - gchar const* request, - GError** error ); -static JsonNode* foobar_workspace_service_send_json_request ( FoobarWorkspaceService* self, - gchar const* request, - GError** error ); -static void foobar_workspace_service_initialize ( FoobarWorkspaceService* self ); -static void foobar_workspace_service_update_active ( FoobarWorkspaceService* self, - GListStore* workspaces ); -static void foobar_workspace_service_add_workspace ( FoobarWorkspaceService* self, - GListStore* workspaces, - JsonObject* workspace_object, - gboolean is_persistent ); -static FoobarWorkspace* foobar_workspace_service_find_workspace ( GListModel* workspaces, - gint64 id, - guint* out_index ); -static gboolean foobar_workspace_service_is_invalid_name ( gchar const* name ); -static gint64 foobar_workspace_service_parse_id ( gchar const* str ); -static gchar* foobar_workspace_service_get_hyprland_base_path ( void ); -static gint foobar_workspace_service_compare_ids ( gconstpointer a, - gconstpointer b ); -static gint foobar_workspace_service_sort_func ( gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarWorkspaceService, foobar_workspace_service, G_TYPE_OBJECT ) - -// -// EventData: -// -// Custom userdata passed to hyprland event handlers in a single pointer. -// - -struct _EventData -{ - FoobarWorkspaceService* service; - gchar* name; - gchar* payload; -}; - -static EventData* event_data_new ( FoobarWorkspaceService* service, - gchar const* name, - gchar const* payload ); -static void event_data_free( EventData* self ); - -// -// EventHandler: -// -// Description for the handler of a single hyprland event. -// - -struct _EventHandler -{ - gchar const* name; - gboolean ( *fn )( EventData* data ); -}; - -// -// Static list of registered event handlers. -// -static EventHandler const event_handlers[] = - { - { .name = "createworkspacev2", .fn = foobar_workspace_service_handle_workspace_created }, - { .name = "destroyworkspacev2", .fn = foobar_workspace_service_handle_workspace_destroyed }, - { .name = "moveworkspacev2", .fn = foobar_workspace_service_handle_workspace_moved }, - { .name = "renameworkspace", .fn = foobar_workspace_service_handle_workspace_renamed }, - { .name = "workspace", .fn = foobar_workspace_service_handle_workspace_activated }, - { .name = "activespecial", .fn = foobar_workspace_service_handle_special_workspace_activated }, - { .name = "focusedmon", .fn = foobar_workspace_service_handle_monitor_focused }, - { .name = "urgent", .fn = foobar_workspace_service_handle_window_urgent }, - { .name = "configreloaded", .fn = foobar_workspace_service_handle_config_reloaded }, - }; - -// --------------------------------------------------------------------------------------------------------------------- -// Workspace -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for workspaces. -// -void foobar_workspace_class_init( FoobarWorkspaceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_workspace_get_property; - object_klass->finalize = foobar_workspace_finalize; - - workspace_props[WORKSPACE_PROP_ID] = g_param_spec_int64( - "id", - "ID", - "Numeric ID of the workspace.", - INT64_MIN, - INT64_MAX, - 0, - G_PARAM_READABLE ); - workspace_props[WORKSPACE_PROP_NAME] = g_param_spec_string( - "name", - "Name", - "Optional human-readable workspace name.", - NULL, - G_PARAM_READABLE ); - workspace_props[WORKSPACE_PROP_MONITOR] = g_param_spec_string( - "monitor", - "Monitor", - "Connector ID of the workspace's monitor.", - NULL, - G_PARAM_READABLE ); - workspace_props[WORKSPACE_PROP_FLAGS] = g_param_spec_flags( - "flags", - "Flags", - "Flags indicating the current state of the workspace.", - FOOBAR_TYPE_WORKSPACE_FLAGS, - FOOBAR_WORKSPACE_FLAGS_NONE, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_WORKSPACE_PROPS, workspace_props ); -} - -// -// Instance initialization for workspaces. -// -void foobar_workspace_init( FoobarWorkspace* self ) -{ - (void)self; -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_workspace_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarWorkspace* self = (FoobarWorkspace*)object; - - switch ( prop_id ) - { - case WORKSPACE_PROP_ID: - g_value_set_int64( value, foobar_workspace_get_id( self ) ); - break; - case WORKSPACE_PROP_NAME: - g_value_set_string( value, foobar_workspace_get_name( self ) ); - break; - case WORKSPACE_PROP_MONITOR: - g_value_set_string( value, foobar_workspace_get_monitor( self ) ); - break; - case WORKSPACE_PROP_FLAGS: - g_value_set_flags( value, foobar_workspace_get_flags( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for workspaces. -// -void foobar_workspace_finalize( GObject* object ) -{ - FoobarWorkspace* self = (FoobarWorkspace*)object; - - g_clear_pointer( &self->name, g_free ); - g_clear_pointer( &self->monitor, g_free ); - - G_OBJECT_CLASS( foobar_workspace_parent_class )->finalize( object ); -} - -// -// Create a new workspace that is owned by the given service (captured as an unowned reference). -// -FoobarWorkspace* foobar_workspace_new( FoobarWorkspaceService* service ) -{ - FoobarWorkspace* self = g_object_new( FOOBAR_TYPE_WORKSPACE, NULL ); - self->service = service; - return self; -} - -// -// Numeric ID of the workspace. -// -gint64 foobar_workspace_get_id( FoobarWorkspace* self ) -{ - g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), 0 ); - return self->id; -} - -// -// Optional human-readable workspace name. -// -gchar const* foobar_workspace_get_name( FoobarWorkspace* self ) -{ - g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), NULL ); - return self->name; -} - -// -// Connector ID of the workspace's monitor. -// -gchar const* foobar_workspace_get_monitor( FoobarWorkspace* self ) -{ - g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), NULL ); - return self->monitor; -} - -// -// Flags indicating the current state of the workspace. -// -FoobarWorkspaceFlags foobar_workspace_get_flags( FoobarWorkspace* self ) -{ - g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), FOOBAR_WORKSPACE_FLAGS_NONE ); - return self->flags; -} - -// -// Numeric ID of the workspace. -// -void foobar_workspace_set_id( - FoobarWorkspace* self, - gint64 value ) -{ - g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); - - if ( self->id != value ) - { - self->id = value; - g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_ID] ); - } -} - -// -// Optional human-readable workspace name. -// -void foobar_workspace_set_name( - FoobarWorkspace* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); - - if ( g_strcmp0( self->name, value ) ) - { - g_clear_pointer( &self->name, g_free ); - self->name = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_NAME] ); - } -} - -// -// Connector ID of the workspace's monitor. -// -void foobar_workspace_set_monitor( - FoobarWorkspace* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); - - if ( g_strcmp0( self->monitor, value ) ) - { - g_clear_pointer( &self->monitor, g_free ); - self->monitor = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_MONITOR] ); - } -} - -// -// Flags indicating the current state of the workspace. -// -void foobar_workspace_set_flags( - FoobarWorkspace* self, - FoobarWorkspaceFlags value ) -{ - g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); - - if ( self->flags != value ) - { - self->flags = value; - g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_FLAGS] ); - } -} - -// -// Let this workspace become the active one. -// -void foobar_workspace_activate( FoobarWorkspace* self ) -{ - g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); - if ( !self->service ) { return; } - - g_autofree gchar* request = NULL; - if ( foobar_workspace_get_id( self ) > 0 ) - { - // Normal - request = g_strdup_printf( "dispatch workspace %lld", (long long)foobar_workspace_get_id( self ) ); - } - else if ( !( foobar_workspace_get_flags( self ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) ) - { - // Named (this includes persistent) - request = g_strdup_printf( "dispatch workspace name:%s", foobar_workspace_get_name( self ) ); - } - else if ( foobar_workspace_get_id( self ) != -99 ) - { - // Named special - request = g_strdup_printf( "dispatch togglespecialworkspace %s", foobar_workspace_get_name( self ) ); - } - else - { - // Special - request = g_strdup( "dispatch togglespecialworkspace" ); - } - - g_autoptr( GError ) error = NULL; - g_free( foobar_workspace_service_send_request( self->service, request, &error ) ); - if ( error ) - { - g_warning( "Unable to activate workspace: %s", error->message ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Service Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for the workspace service. -// -void foobar_workspace_service_class_init( FoobarWorkspaceServiceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_workspace_service_get_property; - object_klass->dispose = foobar_workspace_service_dispose; - object_klass->finalize = foobar_workspace_service_finalize; - - props[PROP_WORKSPACES] = g_param_spec_object( - "workspaces", - "Workspaces", - "Sorted list of all workspaces.", - G_TYPE_LIST_MODEL, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); - - signals[SIGNAL_MONITOR_CONFIGURATION_CHANGED] = g_signal_new( - "monitor-configuration-changed", - FOOBAR_TYPE_WORKSPACE_SERVICE, - G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, - 0, - NULL, - NULL, - NULL, - G_TYPE_NONE, - 0 ); -} - -// -// Instance initialization for the workspace service. -// -void foobar_workspace_service_init( FoobarWorkspaceService* self ) -{ - self->workspaces = g_list_store_new( FOOBAR_TYPE_WORKSPACE ); - - GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_workspace_service_sort_func, NULL, NULL ); - self->sorted_workspaces = gtk_sort_list_model_new( - G_LIST_MODEL( g_object_ref( self->workspaces ) ), - GTK_SORTER( sorter ) ); - - g_autofree gchar* base_path = foobar_workspace_service_get_hyprland_base_path( ); - if ( !base_path ) - { - g_warning( "Unable to connect to hyprland -- is it currently running?" ); - return; - } - - self->tx_path = g_strdup_printf( "%s/.socket.sock", base_path ); - self->rx_path = g_strdup_printf( "%s/.socket2.sock", base_path ); - self->event_cancellable = g_cancellable_new( ); - self->event_thread = g_thread_new( "workspace-event-listener", foobar_workspace_service_event_thread_func, self ); - - foobar_workspace_service_initialize( self ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_workspace_service_get_property( GObject* object, guint prop_id, GValue* value, GParamSpec* pspec ) -{ - FoobarWorkspaceService* self = (FoobarWorkspaceService*)object; - - switch ( prop_id ) - { - case PROP_WORKSPACES: - g_value_set_object( value, foobar_workspace_service_get_workspaces( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance de-initialization for the workspace service. -// -// This is where the event thread is joined because it might still create new object references which is a bad thing to -// do in finalize. -// -void foobar_workspace_service_dispose( GObject* object ) -{ - FoobarWorkspaceService* self = (FoobarWorkspaceService*)object; - - if ( self->event_thread ) - { - g_cancellable_cancel( self->event_cancellable ); - g_thread_join( self->event_thread ); - self->event_thread = NULL; - } - - G_OBJECT_CLASS( foobar_workspace_service_parent_class )->dispose( object ); -} - -// -// Instance cleanup for the workspace service. -// -void foobar_workspace_service_finalize( GObject* object ) -{ - FoobarWorkspaceService* self = (FoobarWorkspaceService*)object; - - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->workspaces ) ); ++i ) - { - FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( self->workspaces ), i ); - workspace->service = NULL; - } - g_clear_object( &self->event_cancellable ); - g_clear_object( &self->sorted_workspaces ); - g_clear_object( &self->workspaces ); - g_clear_pointer( &self->rx_path, g_free ); - g_clear_pointer( &self->tx_path, g_free ); - - G_OBJECT_CLASS( foobar_workspace_service_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new workspace service instance. -// -FoobarWorkspaceService* foobar_workspace_service_new( void ) -{ - return g_object_new( FOOBAR_TYPE_WORKSPACE_SERVICE, NULL ); -} - -// -// Get a sorted list of all workspaces. -// -GListModel* foobar_workspace_service_get_workspaces( FoobarWorkspaceService* self ) -{ - g_return_val_if_fail( FOOBAR_IS_WORKSPACE_SERVICE( self ), NULL ); - return G_LIST_MODEL( self->sorted_workspaces ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Event Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Handler for the "createworkspacev2" event. -// -// The payload has the format "," where is the numeric ID. -// -gboolean foobar_workspace_service_handle_workspace_created( EventData* data ) -{ - gchar* save; - gchar const* id_str = strtok_r( data->payload, ",", &save ); - gchar const* name = strtok_r( NULL, ",", &save ); - g_return_val_if_fail( id_str && name, G_SOURCE_REMOVE ); - - if ( foobar_workspace_service_is_invalid_name( name ) ) { return G_SOURCE_REMOVE; } - gint64 id = foobar_workspace_service_parse_id( id_str ); - - // Get additional information about the workspace. - - g_autoptr( GError ) error = NULL; - g_autoptr( JsonNode ) workspaces_node = foobar_workspace_service_send_json_request( - data->service, - "j/workspaces", - &error ); - if ( !workspaces_node ) - { - g_warning( "Unable to load workspaces: %s", error->message ); - return G_SOURCE_REMOVE; - } - - JsonArray* workspaces_array = json_node_get_array( workspaces_node ); - JsonObject* workspace_object = NULL; - for ( guint i = 0; i < json_array_get_length( workspaces_array ); ++i ) - { - JsonObject* it = json_array_get_object_element( workspaces_array, i ); - if ( id == json_object_get_int_member( it, "id" ) ) - { - workspace_object = it; - break; - } - } - - if ( !workspace_object ) - { - g_warning( "Unable to find new workspace." ); - return G_SOURCE_REMOVE; - } - - // Check if the workspace is persistent by looking for a rule with its ID. - - gboolean is_persistent = FALSE; - - g_autoptr( JsonNode ) rules_node = foobar_workspace_service_send_json_request( - data->service, - "j/workspacerules", - &error ); - if ( !rules_node ) - { - g_warning( "Unable to load workspace rules: %s", error->message ); - return G_SOURCE_REMOVE; - } - - JsonArray* rules_array = json_node_get_array( rules_node ); - for ( guint i = 0; i < json_array_get_length( rules_array ); ++i ) - { - JsonObject* rule_object = json_array_get_object_element( rules_array, i ); - gchar const* rule_id_str = json_object_get_string_member_with_default( rule_object, "workspaceString", NULL ); - if ( id == foobar_workspace_service_parse_id( rule_id_str ) ) - { - is_persistent = json_object_get_boolean_member_with_default( rule_object, "persistent", FALSE ); - break; - } - } - - foobar_workspace_service_add_workspace( data->service, data->service->workspaces, workspace_object, is_persistent ); - foobar_workspace_service_update_active( data->service, data->service->workspaces ); - return G_SOURCE_REMOVE; -} - -// -// Handler for the "destroyworkspacev2" event. -// -// The payload has the format "," where is the numeric ID. -// -gboolean foobar_workspace_service_handle_workspace_destroyed( EventData* data ) -{ - gchar* save; - gchar const* id_str = strtok_r( data->payload, ",", &save ); - gchar const* name = strtok_r( NULL, ",", &save ); - g_return_val_if_fail( id_str && name, G_SOURCE_REMOVE ); - - if ( foobar_workspace_service_is_invalid_name( name ) ) { return G_SOURCE_REMOVE; } - gint64 id = foobar_workspace_service_parse_id( id_str ); - - GListStore* workspaces = data->service->workspaces; - guint index; - FoobarWorkspace* workspace = foobar_workspace_service_find_workspace( G_LIST_MODEL( workspaces ), id, &index ); - if ( workspace ) - { - workspace->service = NULL; - g_list_store_remove( workspaces, index ); - } - - return G_SOURCE_REMOVE; -} - -// -// Handler for the "moveworkspacev2" event. -// -// The payload has the format ",," where is the numeric ID of the workspace. -// -gboolean foobar_workspace_service_handle_workspace_moved( EventData* data ) -{ - gchar* save; - gchar const* workspace_id_str = strtok_r( data->payload, ",", &save ); - gchar const* workspace_name = strtok_r( NULL, ",", &save ); - gchar const* monitor_name = strtok_r( NULL, ",", &save ); - g_return_val_if_fail( workspace_id_str && workspace_name && monitor_name, G_SOURCE_REMOVE ); - - gint64 workspace_id = foobar_workspace_service_parse_id( workspace_id_str ); - FoobarWorkspace* workspace = foobar_workspace_service_find_workspace( - G_LIST_MODEL( data->service->workspaces ), - workspace_id, - NULL ); - if ( workspace ) - { - foobar_workspace_set_monitor( workspace, monitor_name ); - g_signal_emit( data->service, signals[SIGNAL_MONITOR_CONFIGURATION_CHANGED], 0 ); - } - foobar_workspace_service_update_active( data->service, data->service->workspaces ); - - return G_SOURCE_REMOVE; -} - -// -// Handler for the "renameworkspace" event. -// -// The payload has the format "," where is the numeric ID of the workspace. -// -gboolean foobar_workspace_service_handle_workspace_renamed( EventData* data ) -{ - gchar* save; - gchar const* id_str = strtok_r( data->payload, ",", &save ); - gchar const* new_name = strtok_r( NULL, ",", &save ); - g_return_val_if_fail( id_str && new_name, G_SOURCE_REMOVE ); - - gint64 id = foobar_workspace_service_parse_id( id_str ); - FoobarWorkspace* workspace = foobar_workspace_service_find_workspace( - G_LIST_MODEL( data->service->workspaces ), - id, - NULL ); - if ( workspace ) - { - foobar_workspace_set_name( workspace, new_name ); - gtk_sorter_changed( - gtk_sort_list_model_get_sorter( data->service->sorted_workspaces ), - GTK_SORTER_CHANGE_DIFFERENT ); - } - - return G_SOURCE_REMOVE; -} - -// -// Handler for the "workspace" event. -// -gboolean foobar_workspace_service_handle_workspace_activated( EventData* data ) -{ - foobar_workspace_service_update_active( data->service, data->service->workspaces ); - - return G_SOURCE_REMOVE; -} - -// -// Handler for the "activespecial" event. -// -gboolean foobar_workspace_service_handle_special_workspace_activated( EventData* data ) -{ - foobar_workspace_service_update_active( data->service, data->service->workspaces ); - - return G_SOURCE_REMOVE; -} - -// -// Handler for the "focusedmon" event. -// -gboolean foobar_workspace_service_handle_monitor_focused( EventData* data ) -{ - foobar_workspace_service_update_active( data->service, data->service->workspaces ); - - return G_SOURCE_REMOVE; -} - -// -// Handler for the "urgent" event. -// -// The payload has the format "
" where
is the sender window's address. -// -gboolean foobar_workspace_service_handle_window_urgent( EventData* data ) -{ - gchar const* address = data->payload; - - // Find the client with the given address. - - g_autoptr( GError ) error = NULL; - g_autoptr( JsonNode ) clients_node = foobar_workspace_service_send_json_request( - data->service, - "j/clients", - &error ); - if ( !clients_node ) - { - g_warning( "Unable to load clients: %s", error->message ); - return G_SOURCE_REMOVE; - } - - JsonArray* clients_array = json_node_get_array( clients_node ); - JsonObject* client_object = NULL; - for ( guint i = 0; i < json_array_get_length( clients_array ); ++i ) - { - JsonObject* it = json_array_get_object_element( clients_array, i ); - gchar const* it_address = json_object_get_string_member_with_default( it, "address", "" ); - if ( g_str_has_suffix( it_address, address ) ) - { - client_object = it; - break; - } - } - - if ( !client_object ) { return G_SOURCE_REMOVE; } - - // Find the workspace this window belongs to and mark it as "urgent". - - JsonObject* workspace_object = json_object_get_object_member( client_object, "workspace" ); - gint64 id = json_object_get_int_member( workspace_object, "id" ); - FoobarWorkspace* workspace = foobar_workspace_service_find_workspace( - G_LIST_MODEL( data->service->workspaces ), - id, - NULL ); - if ( workspace ) - { - FoobarWorkspaceFlags flags = foobar_workspace_get_flags( workspace ) | FOOBAR_WORKSPACE_FLAGS_URGENT; - foobar_workspace_set_flags( workspace, flags ); - } - - return G_SOURCE_REMOVE; -} - -// -// Handler for the "configreloaded" event. -// -gboolean foobar_workspace_service_handle_config_reloaded( EventData* data ) -{ - foobar_workspace_service_initialize( data->service ); - - return G_SOURCE_REMOVE; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Methods -// --------------------------------------------------------------------------------------------------------------------- - -// -// Implementation of the event thread. -// -// This thread listens on hyprland's event socket (socket.2.sock) for incoming event notifications and then submits -// their registered handlers (if any) to the main loop. -// -gpointer foobar_workspace_service_event_thread_func( gpointer userdata ) -{ - FoobarWorkspaceService* self = (FoobarWorkspaceService*)userdata; - - g_autoptr( GError ) error = NULL; - g_autoptr( GSocket ) sock = g_socket_new( - G_SOCKET_FAMILY_UNIX, - G_SOCKET_TYPE_STREAM, - G_SOCKET_PROTOCOL_DEFAULT, - &error ); - if ( !sock ) - { - g_warning( "Unable to create socket for hyprland communication: %s", error->message ); - return NULL; - } - - g_autoptr( GSocketAddress ) addr = g_unix_socket_address_new( self->rx_path ); - if ( !g_socket_connect( sock, addr, self->event_cancellable, &error ) && - !g_cancellable_is_cancelled( self->event_cancellable ) ) - { - g_warning( "Unable to connect socket to hyprland: %s", error->message ); - return NULL; - } - - // Each event is sent as a complete line, separated by a newline character. - - GString* prev_buf = NULL; // for longer, incomplete messages - gchar buf[1024]; - while ( !g_cancellable_is_cancelled( self->event_cancellable ) ) - { - // Synchronously receive data from the socket, but support cancellation using Gio. - - gssize received = g_socket_receive( sock, buf, sizeof( buf ), self->event_cancellable, &error ); - if ( error && !g_cancellable_is_cancelled( self->event_cancellable ) ) - { - g_warning( "Unable to receive events from hyprland: %s", error->message ); - } - if ( received <= 0 ) { break; } - - gchar* start = buf; - gchar* end = start + received; - while ( start < end ) - { - gchar* next_newline = g_strstr_len( start, end - start, "\n" ); - if ( !next_newline ) - { - // Incomplete message (missing newline) -> store it in the buffer - - prev_buf = ( prev_buf != NULL ) - ? g_string_append_len( prev_buf, start, end - start ) - : g_string_new_len( start, end - start ); - break; - } - - if ( prev_buf ) - { - // Prepend buffered data before the actual data. - - prev_buf = g_string_append_len( prev_buf, start, next_newline - start ); - foobar_workspace_service_dispatch_event( self, prev_buf->str ); - - g_string_free( prev_buf, TRUE ); - prev_buf = NULL; - } - else - { - // Skip allocation and use the buffer contents on the stack directly. - - *next_newline = '\0'; - foobar_workspace_service_dispatch_event( self, start ); - } - - start = next_newline + 1; - } - } - - if ( prev_buf ) { g_string_free( prev_buf, TRUE ); } - - return NULL; -} - -// -// Called when an entire event has been received on the event thread. -// -// This will look through the registered handlers and (if found) invoke the event's handler on the main thread. -// -void foobar_workspace_service_dispatch_event( - FoobarWorkspaceService* self, - gchar* message ) -{ - gchar* delimiter = strstr( message, ">>" ); - if ( !delimiter ) - { - g_warning( "Received an invalid message from hyprland: %s", message ); - return; - } - - *delimiter = '\0'; - gchar* name = message; - gchar* payload = delimiter + 2; - for ( gsize i = 0; i < G_N_ELEMENTS( event_handlers ); ++i ) - { - EventHandler handler = event_handlers[i]; - if ( !strcmp( name, handler.name ) ) - { - g_idle_add_full( - G_PRIORITY_DEFAULT, - G_SOURCE_FUNC( handler.fn ), - event_data_new( self, name, payload ), - (GDestroyNotify)event_data_free ); - break; - } - } -} - -// -// Synchronously send a command to hyprland's control socket (socket.sock), returning the entire response. -// -gchar* foobar_workspace_service_send_request( - FoobarWorkspaceService* self, - gchar const* request, - GError** error ) -{ - g_autoptr( GSocket ) sock = g_socket_new( - G_SOCKET_FAMILY_UNIX, - G_SOCKET_TYPE_STREAM, - G_SOCKET_PROTOCOL_DEFAULT, - error ); - if ( !sock ) { return NULL; } - - g_autoptr( GSocketAddress ) addr = g_unix_socket_address_new( self->tx_path ); - if ( !g_socket_connect( sock, addr, NULL, error ) ) { return NULL; } - - if ( g_socket_send( sock, request, strlen( request ), NULL, error ) == -1 ) { return NULL; } - - GString* str = g_string_sized_new( 0 ); - gchar buf[8192]; - while ( TRUE ) - { - gssize received = g_socket_receive( sock, buf, sizeof( buf ), self->event_cancellable, error ); - if ( received < 0 ) { return g_string_free( str, TRUE ); } - - if ( received == 0 ) { break; } - str = g_string_append_len( str, buf, received ); - } - - return g_string_free( str, FALSE ); -} - -// -// Synchronously send a command to hyprland's control socket (socket.sock), returning the parsed the JSON response. -// -JsonNode* foobar_workspace_service_send_json_request( - FoobarWorkspaceService* self, - gchar const* request, - GError** error ) -{ - g_autofree gchar* json = foobar_workspace_service_send_request( self, request, error ); - if ( !json ) { return NULL; } - - g_autoptr( JsonParser ) parser = json_parser_new( ); - if ( !json_parser_load_from_data( parser, json, (gssize)strlen( json ), error ) ) { return NULL; } - - return json_parser_steal_root( parser ); -} - -// -// Refresh the list of workspaces by actively sending a request to hyprland. -// -void foobar_workspace_service_initialize( FoobarWorkspaceService* self ) -{ - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->workspaces ) ); ++i ) - { - FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( self->workspaces ), i ); - workspace->service = NULL; - } - g_list_store_remove_all( self->workspaces ); - g_autoptr( GListStore ) workspaces = g_list_store_new( FOOBAR_TYPE_WORKSPACE ); - - { - g_autoptr( GError ) error = NULL; - g_autoptr( JsonNode ) workspaces_node = foobar_workspace_service_send_json_request( - self, - "j/workspaces", - &error ); - if ( !workspaces_node ) - { - g_warning( "Unable to load workspaces: %s", error->message ); - return; - } - - JsonArray* workspaces_array = json_node_get_array( workspaces_node ); - for ( guint i = 0; i < json_array_get_length( workspaces_array ); ++i ) - { - JsonObject* workspace_object = json_array_get_object_element( workspaces_array, i ); - foobar_workspace_service_add_workspace( self, workspaces, workspace_object, FALSE ); - } - } - - { - // Load configured persistent workspaces. - - g_autoptr( GError ) error = NULL; - g_autoptr( JsonNode ) rules_node = foobar_workspace_service_send_json_request( - self, - "j/workspacerules", - &error ); - if ( rules_node ) - { - JsonArray* rules_array = json_node_get_array( rules_node ); - for ( guint i = 0; i < json_array_get_length( rules_array ); ++i ) - { - JsonObject* rule_object = json_array_get_object_element( rules_array, i ); - gchar const* name = json_object_get_string_member_with_default( rule_object, "workspaceString", NULL ); - if ( !name ) - { - g_warning( "Found an invalid workspaceString value, skipping." ); - continue; - } - - if ( !json_object_get_boolean_member_with_default( rule_object, "persistent", FALSE ) ) { continue; } - - gint64 id = foobar_workspace_service_parse_id( name ); - json_object_set_int_member( rule_object, "id", id ); - json_object_set_string_member( rule_object, "name", name ); - - foobar_workspace_service_add_workspace( self, workspaces, rule_object, TRUE ); - } - } - else { g_warning( "Unable to load workspace rules: %s", error->message ); } - } - - foobar_workspace_service_update_active( self, workspaces ); - - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( workspaces ) ); ++i ) - { - FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( workspaces ), i ); - g_list_store_append( self->workspaces, workspace ); - } -} - -// -// Update the currently active/visible workspaces by actively sending a request to hyprland. -// -void foobar_workspace_service_update_active( - FoobarWorkspaceService* self, - GListStore* workspaces ) -{ - g_autoptr( GArray ) visible_workspace_ids = g_array_new( FALSE, FALSE, sizeof( gint64 ) ); - gint64 active_workspace_id = 0; - gint64 active_special_workspace_id = 0; - - { - // Get all active workspaces - - g_autoptr( GError ) error = NULL; - g_autoptr( JsonNode ) monitors_node = foobar_workspace_service_send_json_request( - self, - "j/monitors", - &error ); - if ( monitors_node ) - { - JsonArray* monitors_array = json_node_get_array( monitors_node ); - for ( guint i = 0; i < json_array_get_length( monitors_array ); ++i ) - { - JsonObject* monitor_object = json_array_get_object_element( monitors_array, i ); - gint64 workspace_id = 0; - gint64 special_workspace_id = 0; - - if ( json_object_has_member( monitor_object, "activeWorkspace" ) ) - { - JsonObject* workspace_object = json_object_get_object_member( monitor_object, "activeWorkspace" ); - workspace_id = json_object_get_int_member_with_default( workspace_object, "id", 0 ); - if ( workspace_id ) { g_array_append_val( visible_workspace_ids, workspace_id ); } - } - - if ( json_object_has_member( monitor_object, "specialWorkspace" ) ) - { - JsonObject* workspace_object = json_object_get_object_member( monitor_object, "specialWorkspace" ); - special_workspace_id = json_object_get_int_member_with_default( workspace_object, "id", 0 ); - if ( special_workspace_id ) { g_array_append_val( visible_workspace_ids, special_workspace_id ); } - } - - // If this is the focused monitor, then the visible workspace is also the active one. - - if ( json_object_get_boolean_member_with_default( monitor_object, "focused", FALSE ) ) - { - active_workspace_id = workspace_id; - active_special_workspace_id = special_workspace_id; - } - } - } - else { g_warning( "Unable to load monitors: %s", error->message ); } - } - - // Sort visible workspaces to allow binary search. - - g_array_sort( visible_workspace_ids, foobar_workspace_service_compare_ids ); - - // Go through all workspaces and update their state. - - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( workspaces ) ); ++i ) - { - FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( workspaces ), i ); - FoobarWorkspaceFlags flags = foobar_workspace_get_flags( workspace ); - gint64 id = foobar_workspace_get_id( workspace ); - - // Active - gboolean is_active = id == active_workspace_id || id == active_special_workspace_id; - flags = is_active ? flags | FOOBAR_WORKSPACE_FLAGS_ACTIVE : flags & ~FOOBAR_WORKSPACE_FLAGS_ACTIVE; - - // Disable urgency if workspace is active - if ( is_active ) { flags = flags & ~FOOBAR_WORKSPACE_FLAGS_URGENT; } - - // Visible - gboolean is_visible = g_array_binary_search( - visible_workspace_ids, - &id, - foobar_workspace_service_compare_ids, - NULL ); - flags = is_visible ? flags | FOOBAR_WORKSPACE_FLAGS_VISIBLE : flags & ~FOOBAR_WORKSPACE_FLAGS_VISIBLE; - - foobar_workspace_set_flags( workspace, flags ); - } -} - -// -// Add or update a workspace described by a JSON object. -// -void foobar_workspace_service_add_workspace( - FoobarWorkspaceService* self, - GListStore* workspaces, - JsonObject* workspace_object, - gboolean is_persistent ) -{ - gchar const* name = json_object_get_string_member( workspace_object, "name" ); - gint64 id = json_object_get_int_member( workspace_object, "id" ); - gboolean is_special = g_str_has_prefix( name, "special" ); - if ( g_str_has_prefix( name, "name:" ) ) { name += strlen( "name:" ); } - else if ( g_str_has_prefix( name, "special:" ) ) { name += strlen( "special:" ); } - - FoobarWorkspace* existing = foobar_workspace_service_find_workspace( G_LIST_MODEL( workspaces ), id, NULL ); - if ( existing ) - { - // Only update persistence. - - FoobarWorkspaceFlags flags = foobar_workspace_get_flags( existing ); - flags = is_persistent ? flags | FOOBAR_WORKSPACE_FLAGS_PERSISTENT : flags & ~FOOBAR_WORKSPACE_FLAGS_PERSISTENT; - foobar_workspace_set_flags( existing, flags ); - } - else - { - // Found a new workspace -> create a new object for it. - - FoobarWorkspaceFlags flags = FOOBAR_WORKSPACE_FLAGS_NONE; - flags = is_persistent ? flags | FOOBAR_WORKSPACE_FLAGS_PERSISTENT : flags; - flags = is_special ? flags | FOOBAR_WORKSPACE_FLAGS_SPECIAL : flags; - - g_autoptr( FoobarWorkspace ) workspace = foobar_workspace_new( self ); - foobar_workspace_set_id( workspace, id ); - foobar_workspace_set_name( workspace, name ); - foobar_workspace_set_monitor( workspace, json_object_get_string_member( workspace_object, "monitor" ) ); - foobar_workspace_set_flags( workspace, flags ); - g_list_store_append( workspaces, workspace ); - } -} - -// -// Find an existing workspace object by its ID. -// -FoobarWorkspace* foobar_workspace_service_find_workspace( - GListModel* workspaces, - gint64 id, - guint* out_index ) -{ - for ( guint i = 0; i < g_list_model_get_n_items( workspaces ); ++i ) - { - FoobarWorkspace* workspace = g_list_model_get_item( workspaces, i ); - if ( foobar_workspace_get_id( workspace ) == id ) - { - if ( out_index ) { *out_index = i; } - return workspace; - } - } - - return NULL; -} - -// -// Hyprland's IPC sometimes reports the creation of workspaces strangely named `special:special:`. This -// function checks for that and is used to avoid creating (and then removing) such workspaces. -// -// See hyprwm/Hyprland#3424 for more info. -// -gboolean foobar_workspace_service_is_invalid_name( gchar const* name ) -{ - return g_str_has_prefix( name, "special:special:" ); -} - -// -// Parse an ID string and return its numeric code. -// -gint64 foobar_workspace_service_parse_id( gchar const* str ) -{ - if ( !g_strcmp0( str, "special" ) ) { return -99; } - - return strtoll( str, NULL, 10 ); -} - -// -// Get the base path for hyprland socket files. -// -gchar* foobar_workspace_service_get_hyprland_base_path( void ) -{ - gchar const* instance_signature = g_getenv( "HYPRLAND_INSTANCE_SIGNATURE" ); - if ( !instance_signature ) - { - return NULL; - } - - // for Hyprland >= 0.40 - gchar* result = g_strdup_printf( "%s/hypr/%s", g_get_user_runtime_dir( ), instance_signature ); - if ( g_file_test( result, G_FILE_TEST_EXISTS ) ) - { - return result; - } - - // for Hyprland < 0.40 - g_free( result ); - result = g_strdup_printf( "%s/hypr/%s", g_get_tmp_dir( ), instance_signature ); - if ( g_file_test( result, G_FILE_TEST_EXISTS ) ) - { - return result; - } - - g_free( result ); - return NULL; -} - -// -// Comparison function for two workspace IDs, used for binary search. -// -gint foobar_workspace_service_compare_ids( - gconstpointer a, - gconstpointer b ) -{ - gint64 const* a_int = a; - gint64 const* b_int = b; - - if ( *a_int < *b_int ) { return -1; } - if ( *a_int > *b_int ) { return 1; } - return 0; -} - -// -// Sorting callback for workspaces. Items are sorted to be in the following order: -// 1. normal -// 2. named persistent -// 3. named -// 4. special -// 5. named special -// -gint foobar_workspace_service_sort_func( - gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ) -{ - (void)userdata; - - FoobarWorkspace* workspace_a = (FoobarWorkspace*)item_a; - FoobarWorkspace* workspace_b = (FoobarWorkspace*)item_b; - gint64 id_a = foobar_workspace_get_id( workspace_a ); - gint64 id_b = foobar_workspace_get_id( workspace_b ); - gboolean special_a = ( foobar_workspace_get_flags( workspace_a ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) != 0; - gboolean special_b = ( foobar_workspace_get_flags( workspace_b ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) != 0; - gboolean named_a = id_a < 0; - gboolean named_b = id_b < 0; - - // both normal (includes numbered persistent) => sort by ID - if ( !named_a && !named_b ) - { - if ( id_a < id_b ) { return -1; } - if ( id_a > id_b ) { return 1; } - return 0; - } - - // one normal, one special => normal first - if ( special_a && !special_b ) { return 1; } - if ( !special_a && special_b ) { return -1; } - - // only one normal, one named - if ( named_a && !named_b ) { return -1; } - if ( !named_a && named_b ) { return 1; } - - // both special - if ( special_a && special_b ) - { - // if one is -99 => put it last - if ( id_a == -99 && id_b != -99 ) { return 1; } - if ( id_a != -99 && id_b == -99 ) { return -1; } - } - - return g_strcmp0( foobar_workspace_get_name( workspace_a ), foobar_workspace_get_name( workspace_b ) ); -} - -// -// Create a new EventData structure. -// -static EventData* event_data_new( - FoobarWorkspaceService* service, - gchar const* name, - gchar const* payload ) -{ - EventData* self = g_new0( EventData, 1 ); - self->service = g_object_ref( service ); - self->name = g_strdup( name ); - self->payload = g_strdup( payload ); - return self; -} - -// -// Destroy an EventData structure. -// -static void event_data_free( EventData* self ) -{ - if ( self ) - { - g_object_unref( self->service ); - g_free( self->name ); - g_free( self->payload ); - g_free( self ); - } -} diff --git a/src/services/workspace-service.h b/src/services/workspace-service.h deleted file mode 100644 index c80e7dc..0000000 --- a/src/services/workspace-service.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_WORKSPACE_FLAGS foobar_workspace_flags_get_type( ) -#define FOOBAR_TYPE_WORKSPACE foobar_workspace_get_type( ) -#define FOOBAR_TYPE_WORKSPACE_SERVICE foobar_workspace_service_get_type( ) - -typedef enum -{ - FOOBAR_WORKSPACE_FLAGS_NONE = 0, - // Indicates that the workspace is a globally active one (i.e. visible on the focused monitor). - FOOBAR_WORKSPACE_FLAGS_ACTIVE = 1 << 0, - // Indicates that the workspace is currently visible. - FOOBAR_WORKSPACE_FLAGS_VISIBLE = 1 << 1, - // Indicates that the workspace is a special workspace. - FOOBAR_WORKSPACE_FLAGS_SPECIAL = 1 << 2, - // Indicates that the workspace is persistent (i.e. configured in the config file). - FOOBAR_WORKSPACE_FLAGS_PERSISTENT = 1 << 3, - // Indicates that the workspace requires an urgent action (e.g. a notification). - FOOBAR_WORKSPACE_FLAGS_URGENT = 1 << 4, -} FoobarWorkspaceFlags; - -GType foobar_workspace_flags_get_type( void ); - -G_DECLARE_FINAL_TYPE( FoobarWorkspace, foobar_workspace, FOOBAR, WORKSPACE, GObject ) - -gint64 foobar_workspace_get_id ( FoobarWorkspace* self ); -gchar const* foobar_workspace_get_name ( FoobarWorkspace* self ); -gchar const* foobar_workspace_get_monitor( FoobarWorkspace* self ); -FoobarWorkspaceFlags foobar_workspace_get_flags ( FoobarWorkspace* self ); -void foobar_workspace_activate ( FoobarWorkspace* self ); - -G_DECLARE_FINAL_TYPE( FoobarWorkspaceService, foobar_workspace_service, FOOBAR, WORKSPACE_SERVICE, GObject ) - -FoobarWorkspaceService* foobar_workspace_service_new ( void ); -GListModel* foobar_workspace_service_get_workspaces( FoobarWorkspaceService* self ); - -G_END_DECLS diff --git a/src/utils.c b/src/utils.c deleted file mode 100644 index dae3e65..0000000 --- a/src/utils.c +++ /dev/null @@ -1,25 +0,0 @@ -#include "utils.h" -#include - -// -// Return the path for a file in the user's XDG cache directory. The result should be freed using g_free. -// -// This will also ensure that the application's cache directory exists. -// -gchar* foobar_get_cache_path( gchar const* filename ) -{ - g_autoptr( GError ) error = NULL; - g_autofree gchar* directory_path = g_strdup_printf( "%s/foobar", g_get_user_cache_dir( ) ); - - if ( !g_file_test( directory_path, G_FILE_TEST_EXISTS ) ) - { - g_autoptr( GFile ) cache_dir = g_file_new_for_path( directory_path ); - if ( !g_file_make_directory_with_parents( cache_dir, NULL, &error ) ) - { - g_warning( "Unable to create cache directory: %s", error->message ); - return NULL; - } - } - - return g_strdup_printf( "%s/%s", directory_path, filename ); -} diff --git a/src/utils.h b/src/utils.h deleted file mode 100644 index 0cd6e38..0000000 --- a/src/utils.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -gchar* foobar_get_cache_path( gchar const* filename ); - -G_END_DECLS diff --git a/src/widgets/control-center/control-button.c b/src/widgets/control-center/control-button.c deleted file mode 100644 index 5b6d991..0000000 --- a/src/widgets/control-center/control-button.c +++ /dev/null @@ -1,496 +0,0 @@ -#include "widgets/control-center/control-button.h" - -// -// FoobarControlButton: -// -// A button in the "controls" section of the control center. This can be a regular button or a toggle button with an -// optional secondary button to toggle a details view. -// - -struct _FoobarControlButton -{ - GtkWidget parent_instance; - gchar* icon_name; - gchar* label; - gboolean can_expand; - gboolean can_toggle; - gboolean is_expanded; - gboolean is_toggled; -}; - -enum -{ - PROP_ICON_NAME = 1, - PROP_LABEL, - PROP_CAN_EXPAND, - PROP_CAN_TOGGLE, - PROP_IS_EXPANDED, - PROP_IS_TOGGLED, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_control_button_class_init ( FoobarControlButtonClass* klass ); -static void foobar_control_button_init ( FoobarControlButton* self ); -static void foobar_control_button_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_control_button_set_property ( GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ); -static void foobar_control_button_dispose ( GObject* object ); -static void foobar_control_button_finalize ( GObject* object ); -static void foobar_control_button_handle_primary_clicked( GtkButton* button, - gpointer userdata ); -static void foobar_control_button_handle_expand_clicked ( GtkButton* button, - gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarControlButton, foobar_control_button, GTK_TYPE_WIDGET ) - -// --------------------------------------------------------------------------------------------------------------------- -// Widget Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for control buttons. -// -void foobar_control_button_class_init( FoobarControlButtonClass* klass ) -{ - GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); - gtk_widget_class_set_css_name( widget_klass, "control-button" ); - gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT ); - - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_control_button_get_property; - object_klass->set_property = foobar_control_button_set_property; - object_klass->dispose = foobar_control_button_dispose; - object_klass->finalize = foobar_control_button_finalize; - - props[PROP_ICON_NAME] = g_param_spec_string( - "icon-name", - "Icon Name", - "Name of the icon to be displayed next to the label.", - NULL, - G_PARAM_READWRITE ); - props[PROP_LABEL] = g_param_spec_string( - "label", - "Label", - "The button's main label.", - NULL, - G_PARAM_READWRITE ); - props[PROP_CAN_EXPAND] = g_param_spec_boolean( - "can-expand", - "Can Expand", - "Indicates whether some details are hidden and can be revealed by a toggle button next to the main action.", - FALSE, - G_PARAM_READWRITE ); - props[PROP_CAN_TOGGLE] = g_param_spec_boolean( - "can-toggle", - "Can Toggle", - "Indicates whether the main button can be toggled.", - FALSE, - G_PARAM_READWRITE ); - props[PROP_IS_EXPANDED] = g_param_spec_boolean( - "is-expanded", - "Is Expanded", - "Indicates whether additional details should be shown.", - FALSE, - G_PARAM_READWRITE ); - props[PROP_IS_TOGGLED] = g_param_spec_boolean( - "is-toggled", - "Is Toggled", - "Indicates whether the main button is currently toggled.", - FALSE, - G_PARAM_READWRITE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for control buttons. -// -void foobar_control_button_init( FoobarControlButton* self ) -{ - GtkWidget* icon = gtk_image_new( ); - gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); - g_object_bind_property( - self, - "icon-name", - icon, - "icon-name", - G_BINDING_SYNC_CREATE ); - - GtkWidget* label = gtk_label_new( NULL ); - gtk_widget_set_valign( label, GTK_ALIGN_CENTER ); - gtk_widget_set_hexpand( label, TRUE ); - gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); - gtk_label_set_xalign( GTK_LABEL( label ), 0 ); - g_object_bind_property( - self, - "label", - label, - "label", - G_BINDING_SYNC_CREATE ); - - GtkWidget* content = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); - gtk_box_append( GTK_BOX( content ), icon ); - gtk_box_append( GTK_BOX( content ), label ); - - GtkWidget* primary_button = gtk_button_new( ); - gtk_widget_add_css_class( primary_button, "primary" ); - gtk_widget_set_hexpand( primary_button, TRUE ); - gtk_button_set_child( GTK_BUTTON( primary_button ), content ); - g_object_bind_property( - self, - "can-toggle", - primary_button, - "sensitive", - G_BINDING_SYNC_CREATE ); - g_signal_connect( primary_button, "clicked", G_CALLBACK( foobar_control_button_handle_primary_clicked ), self ); - - GtkWidget* expand_button = gtk_button_new_from_icon_name( "fluent-chevron-right-symbolic" ); - gtk_widget_add_css_class( expand_button, "expand" ); - g_object_bind_property( - self, - "can-expand", - expand_button, - "visible", - G_BINDING_SYNC_CREATE ); - g_signal_connect( expand_button, "clicked", G_CALLBACK( foobar_control_button_handle_expand_clicked ), self ); - - GtkLayoutManager* layout = gtk_widget_get_layout_manager( GTK_WIDGET( self ) ); - gtk_orientable_set_orientation( GTK_ORIENTABLE( layout ), GTK_ORIENTATION_HORIZONTAL ); - gtk_widget_insert_before( primary_button, GTK_WIDGET( self ), NULL ); - gtk_widget_insert_before( expand_button, GTK_WIDGET( self ), NULL ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_control_button_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarControlButton* self = (FoobarControlButton*)object; - - switch ( prop_id ) - { - case PROP_ICON_NAME: - g_value_set_string( value, foobar_control_button_get_icon_name( self ) ); - break; - case PROP_LABEL: - g_value_set_string( value, foobar_control_button_get_label( self ) ); - break; - case PROP_CAN_EXPAND: - g_value_set_boolean( value, foobar_control_button_can_expand( self ) ); - break; - case PROP_CAN_TOGGLE: - g_value_set_boolean( value, foobar_control_button_can_toggle( self ) ); - break; - case PROP_IS_EXPANDED: - g_value_set_boolean( value, foobar_control_button_is_expanded( self ) ); - break; - case PROP_IS_TOGGLED: - g_value_set_boolean( value, foobar_control_button_is_toggled( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Property setter implementation, mapping a property id to a method. -// -void foobar_control_button_set_property( - GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ) -{ - FoobarControlButton* self = (FoobarControlButton*)object; - - switch ( prop_id ) - { - case PROP_ICON_NAME: - foobar_control_button_set_icon_name( self, g_value_get_string( value ) ); - break; - case PROP_LABEL: - foobar_control_button_set_label( self, g_value_get_string( value ) ); - break; - case PROP_CAN_EXPAND: - foobar_control_button_set_can_expand( self, g_value_get_boolean( value ) ); - break; - case PROP_CAN_TOGGLE: - foobar_control_button_set_can_toggle( self, g_value_get_boolean( value ) ); - break; - case PROP_IS_EXPANDED: - foobar_control_button_set_expanded( self, g_value_get_boolean( value ) ); - break; - case PROP_IS_TOGGLED: - foobar_control_button_set_toggled( self, g_value_get_boolean( value ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance de-initialization for control buttons. -// -void foobar_control_button_dispose( GObject* object ) -{ - FoobarControlButton* self = (FoobarControlButton*)object; - - GtkWidget* child; - while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) - { - gtk_widget_unparent( child ); - } - - G_OBJECT_CLASS( foobar_control_button_parent_class )->dispose( object ); -} - -// -// Instance cleanup for control buttons. -// -void foobar_control_button_finalize( GObject* object ) -{ - FoobarControlButton* self = (FoobarControlButton*)object; - - g_clear_pointer( &self->icon_name, g_free ); - g_clear_pointer( &self->label, g_free ); - - G_OBJECT_CLASS( foobar_control_button_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new control button instance. -// -GtkWidget* foobar_control_button_new( void ) -{ - return g_object_new( FOOBAR_TYPE_CONTROL_BUTTON, NULL ); -} - -// -// Get the name of the icon to be displayed next to the label. -// -gchar const* foobar_control_button_get_icon_name( FoobarControlButton* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), NULL ); - return self->icon_name; -} - -// -// Get the button's main label. -// -gchar const* foobar_control_button_get_label( FoobarControlButton* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), NULL ); - return self->label; -} - -// -// Check whether some details are hidden and can be revealed by a toggle button next to the main action. -// -gboolean foobar_control_button_can_expand( FoobarControlButton* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE ); - return self->can_expand; -} - -// -// Check whether the main button can be toggled. -// -gboolean foobar_control_button_can_toggle( FoobarControlButton* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE ); - return self->can_toggle; -} - -// -// Check whether additional details should currently be shown. -// -gboolean foobar_control_button_is_expanded( FoobarControlButton* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE ); - return self->is_expanded; -} - -// -// Check whether the main button is currently toggled. -// -gboolean foobar_control_button_is_toggled( FoobarControlButton* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE ); - return self->is_toggled; -} - -// -// Update the name of the icon to be displayed next to the label. -// -void foobar_control_button_set_icon_name( - FoobarControlButton* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); - - if ( g_strcmp0( self->icon_name, value ) ) - { - g_clear_pointer( &self->icon_name, g_free ); - self->icon_name = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ICON_NAME] ); - } -} - -// -// Update the button's main label. -// -void foobar_control_button_set_label( - FoobarControlButton* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); - - if ( g_strcmp0( self->label, value ) ) - { - g_clear_pointer( &self->label, g_free ); - self->label = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] ); - } -} - -// -// Update whether some details are hidden and can be revealed by a toggle button next to the main action. -// -void foobar_control_button_set_can_expand( - FoobarControlButton* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); - - if ( !value ) { foobar_control_button_set_expanded( self, FALSE ); } - - value = !!value; - if ( self->can_expand != value ) - { - self->can_expand = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_EXPAND] ); - } -} - -// -// Update whether the main button can be toggled. -// -void foobar_control_button_set_can_toggle( - FoobarControlButton* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); - - if ( !value ) { foobar_control_button_set_toggled( self, FALSE ); } - - value = !!value; - if ( self->can_toggle != value ) - { - self->can_toggle = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_TOGGLE] ); - } -} - -// -// Update whether additional details are currently shown. -// -void foobar_control_button_set_expanded( - FoobarControlButton* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); - - value = value && self->can_expand; - if ( self->is_expanded != value ) - { - self->is_expanded = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] ); - - if ( self->is_expanded ) - { - gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" ); - } - else - { - gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" ); - } - } -} - -// -// Update whether the main button is currently toggled. -// -void foobar_control_button_set_toggled( - FoobarControlButton* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) ); - - value = value && self->can_toggle; - if ( self->is_toggled != value ) - { - self->is_toggled = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_TOGGLED] ); - - if ( self->is_toggled ) - { - gtk_widget_add_css_class( GTK_WIDGET( self ), "toggled" ); - } - else - { - gtk_widget_remove_css_class( GTK_WIDGET( self ), "toggled" ); - } - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called when the primary button was clicked. -// -// If allowed, this will toggle the button. -// -void foobar_control_button_handle_primary_clicked( GtkButton* button, gpointer userdata ) -{ - (void)button; - FoobarControlButton* self = (FoobarControlButton*)userdata; - - if ( foobar_control_button_can_toggle( self ) ) - { - foobar_control_button_set_toggled( self, !foobar_control_button_is_toggled( self ) ); - } -} - -// -// Called when the expand button was clicked. -// -// If allowed, this will toggle the "expanded" state of the button. -// -void foobar_control_button_handle_expand_clicked( GtkButton* button, gpointer userdata ) -{ - (void)button; - FoobarControlButton* self = (FoobarControlButton*)userdata; - - if ( foobar_control_button_can_expand( self ) ) - { - foobar_control_button_set_expanded( self, !foobar_control_button_is_expanded( self ) ); - } -} diff --git a/src/widgets/control-center/control-button.h b/src/widgets/control-center/control-button.h deleted file mode 100644 index d4c3737..0000000 --- a/src/widgets/control-center/control-button.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_CONTROL_BUTTON foobar_control_button_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarControlButton, foobar_control_button, FOOBAR, CONTROL_BUTTON, GtkWidget ) - -GtkWidget* foobar_control_button_new ( void ); -gchar const* foobar_control_button_get_icon_name ( FoobarControlButton* self ); -gchar const* foobar_control_button_get_label ( FoobarControlButton* self ); -gboolean foobar_control_button_can_expand ( FoobarControlButton* self ); -gboolean foobar_control_button_can_toggle ( FoobarControlButton* self ); -gboolean foobar_control_button_is_expanded ( FoobarControlButton* self ); -gboolean foobar_control_button_is_toggled ( FoobarControlButton* self ); -void foobar_control_button_set_icon_name ( FoobarControlButton* self, - gchar const* value ); -void foobar_control_button_set_label ( FoobarControlButton* self, - gchar const* value ); -void foobar_control_button_set_can_expand( FoobarControlButton* self, - gboolean value ); -void foobar_control_button_set_can_toggle( FoobarControlButton* self, - gboolean value ); -void foobar_control_button_set_expanded ( FoobarControlButton* self, - gboolean value ); -void foobar_control_button_set_toggled ( FoobarControlButton* self, - gboolean value ); - -G_END_DECLS diff --git a/src/widgets/control-center/control-details-item.c b/src/widgets/control-center/control-details-item.c deleted file mode 100644 index cb54dd3..0000000 --- a/src/widgets/control-center/control-details-item.c +++ /dev/null @@ -1,307 +0,0 @@ -#include "widgets/control-center/control-details-item.h" - -// -// FoobarControlDetailsAccessory: -// -// The accessory shown next to an item in the control center details list (if any). -// - -G_DEFINE_ENUM_TYPE( - FoobarControlDetailsAccessory, - foobar_control_details_accessory, - G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE, "none" ), - G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS, "progress" ), - G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED, "checked" ) ) - -// -// FoobarControlDetailsItem: -// -// Standard row layout for items in a FoobarControlDetails list. The rows are made up of a label and an accessory that -// is shown or hidden depending on the item's state. -// - -struct _FoobarControlDetailsItem -{ - GtkWidget parent_instance; - GtkWidget* spinner; - GtkWidget* checkmark; - gchar* label; - FoobarControlDetailsAccessory accessory; -}; - -enum -{ - PROP_LABEL = 1, - PROP_ACCESSORY, - PROP_IS_CHECKED, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_control_details_item_class_init ( FoobarControlDetailsItemClass* klass ); -static void foobar_control_details_item_init ( FoobarControlDetailsItem* self ); -static void foobar_control_details_item_get_property( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_control_details_item_set_property( GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ); -static void foobar_control_details_item_dispose ( GObject* object ); -static void foobar_control_details_item_finalize ( GObject* object ); - -G_DEFINE_FINAL_TYPE( FoobarControlDetailsItem, foobar_control_details_item, GTK_TYPE_WIDGET ) - -// --------------------------------------------------------------------------------------------------------------------- -// Widget Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for control details items. -// -void foobar_control_details_item_class_init( FoobarControlDetailsItemClass* klass ) -{ - GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); - gtk_widget_class_set_css_name( widget_klass, "control-details-item" ); - gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT ); - - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_control_details_item_get_property; - object_klass->set_property = foobar_control_details_item_set_property; - object_klass->dispose = foobar_control_details_item_dispose; - object_klass->finalize = foobar_control_details_item_finalize; - - props[PROP_LABEL] = g_param_spec_string( - "label", - "Label", - "Label shown for the item.", - NULL, - G_PARAM_READWRITE ); - props[PROP_ACCESSORY] = g_param_spec_enum( - "accessory", - "Accessory", - "Accessory shown for the item.", - FOOBAR_TYPE_CONTROL_DETAILS_ACCESSORY, - 0, - G_PARAM_READWRITE ); - props[PROP_IS_CHECKED] = g_param_spec_boolean( - "is-checked", - "Is Checked", - "Current checked-state for the item.", - FALSE, - G_PARAM_READWRITE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for control details items. -// -void foobar_control_details_item_init( FoobarControlDetailsItem* self ) -{ - GtkWidget* label = gtk_label_new( NULL ); - gtk_widget_set_hexpand( label, TRUE ); - gtk_widget_set_valign( label, GTK_ALIGN_CENTER ); - gtk_label_set_ellipsize( GTK_LABEL( label ), PANGO_ELLIPSIZE_END ); - gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); - gtk_label_set_xalign( GTK_LABEL( label ), 0 ); - gtk_label_set_wrap( GTK_LABEL( label ), FALSE ); - g_object_bind_property( - self, - "label", - label, - "label", - G_BINDING_SYNC_CREATE ); - - self->spinner = gtk_spinner_new( ); - gtk_spinner_set_spinning( GTK_SPINNER( self->spinner ), TRUE ); - gtk_widget_add_css_class( self->spinner, "accessory" ); - gtk_widget_set_valign( self->spinner, GTK_ALIGN_CENTER ); - gtk_widget_set_visible( self->spinner, FALSE ); - - self->checkmark = gtk_image_new_from_icon_name( "fluent-checkmark-symbolic" ); - gtk_widget_add_css_class( self->checkmark, "checkmark" ); - gtk_widget_add_css_class( self->checkmark, "accessory" ); - gtk_widget_set_valign( self->checkmark, GTK_ALIGN_CENTER ); - gtk_widget_set_visible( self->checkmark, FALSE ); - - gtk_widget_insert_before( label, GTK_WIDGET( self ), NULL ); - gtk_widget_insert_before( self->spinner, GTK_WIDGET( self ), NULL ); - gtk_widget_insert_before( self->checkmark, GTK_WIDGET( self ), NULL ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_control_details_item_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object; - - switch ( prop_id ) - { - case PROP_LABEL: - g_value_set_string( value, foobar_control_details_item_get_label( self ) ); - break; - case PROP_ACCESSORY: - g_value_set_enum( value, foobar_control_details_item_get_accessory( self ) ); - break; - case PROP_IS_CHECKED: - g_value_set_boolean( value, foobar_control_details_item_is_checked( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Property setter implementation, mapping a property id to a method. -// -void foobar_control_details_item_set_property( - GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ) -{ - FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object; - - switch ( prop_id ) - { - case PROP_LABEL: - foobar_control_details_item_set_label( self, g_value_get_string( value ) ); - break; - case PROP_ACCESSORY: - foobar_control_details_item_set_accessory( self, g_value_get_enum( value ) ); - break; - case PROP_IS_CHECKED: - foobar_control_details_item_set_checked( self, g_value_get_boolean( value ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance de-initialization for control details items. -// -void foobar_control_details_item_dispose( GObject* object ) -{ - FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object; - - GtkWidget* child; - while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) - { - gtk_widget_unparent( child ); - } - - G_OBJECT_CLASS( foobar_control_details_item_parent_class )->dispose( object ); -} - -// -// Instance cleanup for control details items. -// -void foobar_control_details_item_finalize( GObject* object ) -{ - FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object; - - g_clear_pointer( &self->label, g_free ); - - G_OBJECT_CLASS( foobar_control_details_item_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new control details item instance. -// -GtkWidget* foobar_control_details_item_new( void ) -{ - return g_object_new( FOOBAR_TYPE_CONTROL_DETAILS_ITEM, NULL ); -} - -// -// Get the label shown for the item. -// -gchar const* foobar_control_details_item_get_label( FoobarControlDetailsItem* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), NULL ); - return self->label; -} - -// -// Get the accessory shown for the item. -// -FoobarControlDetailsAccessory foobar_control_details_item_get_accessory( FoobarControlDetailsItem* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), 0 ); - return self->accessory; -} - -// -// Get the current checked-state for the item. -// -gboolean foobar_control_details_item_is_checked( FoobarControlDetailsItem* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), FALSE ); - return self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED; -} - -// -// Update the label shown for the item. -// -void foobar_control_details_item_set_label( - FoobarControlDetailsItem* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) ); - - if ( g_strcmp0( self->label, value ) ) - { - g_clear_pointer( &self->label, g_free ); - self->label = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] ); - } -} - -// -// Update the current accessory shown for the item. -// -void foobar_control_details_item_set_accessory( - FoobarControlDetailsItem* self, - FoobarControlDetailsAccessory value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) ); - - if ( self->accessory != value ) - { - self->accessory = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ACCESSORY] ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_CHECKED] ); - - gtk_widget_set_visible( self->spinner, self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS ); - gtk_widget_set_visible( self->checkmark, self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED ); - } -} - -// -// Update the current checked-state for the item. -// -void foobar_control_details_item_set_checked( - FoobarControlDetailsItem* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) ); - - FoobarControlDetailsAccessory accessory = value - ? FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED - : FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE; - foobar_control_details_item_set_accessory( self, accessory ); -} diff --git a/src/widgets/control-center/control-details-item.h b/src/widgets/control-center/control-details-item.h deleted file mode 100644 index ff890f9..0000000 --- a/src/widgets/control-center/control-details-item.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_CONTROL_DETAILS_ACCESSORY foobar_control_details_accessory_get_type( ) -#define FOOBAR_TYPE_CONTROL_DETAILS_ITEM foobar_control_details_item_get_type( ) - -typedef enum -{ - FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE, - FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED, - FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS, -} FoobarControlDetailsAccessory; - -GType foobar_control_details_accessory_get_type( void ); - -G_DECLARE_FINAL_TYPE( FoobarControlDetailsItem, foobar_control_details_item, FOOBAR, CONTROL_DETAILS_ITEM, GtkWidget ) - -GtkWidget* foobar_control_details_item_new ( void ); -gchar const* foobar_control_details_item_get_label ( FoobarControlDetailsItem* self ); -FoobarControlDetailsAccessory foobar_control_details_item_get_accessory( FoobarControlDetailsItem* self ); -gboolean foobar_control_details_item_is_checked ( FoobarControlDetailsItem* self ); -void foobar_control_details_item_set_label ( FoobarControlDetailsItem* self, - gchar const* value ); -void foobar_control_details_item_set_accessory( FoobarControlDetailsItem* self, - FoobarControlDetailsAccessory value ); -void foobar_control_details_item_set_checked ( FoobarControlDetailsItem* self, - gboolean value ); - -G_END_DECLS diff --git a/src/widgets/control-center/control-details.c b/src/widgets/control-center/control-details.c deleted file mode 100644 index 72a90aa..0000000 --- a/src/widgets/control-center/control-details.c +++ /dev/null @@ -1,289 +0,0 @@ -#include "widgets/control-center/control-details.h" - -// -// FoobarControlDetails: -// -// A revealer for details in the "controls" section of the control center. This widget is usually used to embed a list -// view. It takes care of scrolling by embedding the child in a GtkScrolledWindow. -// - -struct _FoobarControlDetails -{ - GtkWidget parent_instance; - GtkWidget* child; - gboolean is_expanded; - gint inset_top; -}; - -enum -{ - PROP_CHILD = 1, - PROP_IS_EXPANDED, - PROP_INSET_TOP, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_control_details_class_init ( FoobarControlDetailsClass* klass ); -static void foobar_control_details_init ( FoobarControlDetails* self ); -static void foobar_control_details_get_property( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_control_details_set_property( GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ); -static void foobar_control_details_dispose ( GObject* object ); - -G_DEFINE_FINAL_TYPE( FoobarControlDetails, foobar_control_details, GTK_TYPE_WIDGET ) - -// --------------------------------------------------------------------------------------------------------------------- -// Widget Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for control details. -// -void foobar_control_details_class_init( FoobarControlDetailsClass* klass ) -{ - GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); - gtk_widget_class_set_css_name( widget_klass, "control-details" ); - gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BIN_LAYOUT ); - - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_control_details_get_property; - object_klass->set_property = foobar_control_details_set_property; - object_klass->dispose = foobar_control_details_dispose; - - props[PROP_CHILD] = g_param_spec_object( - "child", - "Child", - "Child widget for the details revealer.", - GTK_TYPE_WIDGET, - G_PARAM_READWRITE ); - props[PROP_IS_EXPANDED] = g_param_spec_boolean( - "is-expanded", - "Is Expanded", - "Current state of the details revealer.", - FALSE, - G_PARAM_READWRITE ); - props[PROP_INSET_TOP] = g_param_spec_int( - "inset-top", - "Inset Top", - "Inset from the top edge (only visible when expanded).", - 0, - INT_MAX, - 0, - G_PARAM_READWRITE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for control details. -// -void foobar_control_details_init( FoobarControlDetails* self ) -{ - GtkWidget* scrolled_window = gtk_scrolled_window_new( ); - gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scrolled_window ), GTK_POLICY_NEVER, GTK_POLICY_EXTERNAL ); - gtk_scrolled_window_set_propagate_natural_height( GTK_SCROLLED_WINDOW( scrolled_window ), TRUE ); - g_object_bind_property( - self, - "child", - scrolled_window, - "child", - G_BINDING_SYNC_CREATE ); - g_object_bind_property( - self, - "inset-top", - scrolled_window, - "margin-top", - G_BINDING_SYNC_CREATE ); - - GtkWidget* revealer = gtk_revealer_new( ); - gtk_revealer_set_transition_type( GTK_REVEALER( revealer ), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN ); - gtk_revealer_set_transition_duration( GTK_REVEALER( revealer ), 100 ); - gtk_revealer_set_child( GTK_REVEALER( revealer ), scrolled_window ); - g_object_bind_property( - self, - "is-expanded", - revealer, - "reveal-child", - G_BINDING_SYNC_CREATE ); - - gtk_widget_set_parent( revealer, GTK_WIDGET( self ) ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_control_details_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarControlDetails* self = (FoobarControlDetails*)object; - - switch ( prop_id ) - { - case PROP_CHILD: - g_value_set_object( value, foobar_control_details_get_child( self ) ); - break; - case PROP_IS_EXPANDED: - g_value_set_boolean( value, foobar_control_details_is_expanded( self ) ); - break; - case PROP_INSET_TOP: - g_value_set_int( value, foobar_control_details_get_inset_top( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Property setter implementation, mapping a property id to a method. -// -void foobar_control_details_set_property( - GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ) -{ - FoobarControlDetails* self = (FoobarControlDetails*)object; - - switch ( prop_id ) - { - case PROP_CHILD: - foobar_control_details_set_child( self, g_value_get_object( value ) ); - break; - case PROP_IS_EXPANDED: - foobar_control_details_set_expanded( self, g_value_get_boolean( value ) ); - break; - case PROP_INSET_TOP: - foobar_control_details_set_inset_top( self, g_value_get_int( value ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance de-initialization for control details. -// -void foobar_control_details_dispose( GObject* object ) -{ - FoobarControlDetails* self = (FoobarControlDetails*)object; - - GtkWidget* child; - while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) - { - gtk_widget_unparent( child ); - } - - g_clear_object( &self->child ); - - G_OBJECT_CLASS( foobar_control_details_parent_class )->dispose( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new control details instance. -// -GtkWidget* foobar_control_details_new( void ) -{ - return g_object_new( FOOBAR_TYPE_CONTROL_DETAILS, NULL ); -} - -// -// Get the child widget for the details revealer. -// -GtkWidget* foobar_control_details_get_child( FoobarControlDetails* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), NULL ); - return self->child; -} - -// -// Get the current state of the details revealer. -// -gboolean foobar_control_details_is_expanded( FoobarControlDetails* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), FALSE ); - return self->is_expanded; -} - -// -// Get the inset from the top edge (only visible when expanded). -// -gint foobar_control_details_get_inset_top( FoobarControlDetails* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), FALSE ); - return self->inset_top; -} - -// -// Update the child widget for the details revealer. -// -void foobar_control_details_set_child( - FoobarControlDetails* self, - GtkWidget* value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) ); - - if ( self->child != value ) - { - g_clear_object( &self->child ); - if ( value ) { self->child = g_object_ref( value ); } - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] ); - } -} - -// -// Update the current state of the details revealer. -// -void foobar_control_details_set_expanded( - FoobarControlDetails* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) ); - - value = !!value; - if ( self->is_expanded != value ) - { - self->is_expanded = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] ); - - if ( self->is_expanded ) - { - gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" ); - } - else - { - gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" ); - } - } -} - -// -// Update the inset from the top edge (only visible when expanded). -// -void foobar_control_details_set_inset_top( - FoobarControlDetails* self, - gint value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) ); - - value = MAX( value, 0 ); - if ( self->inset_top != value ) - { - self->inset_top = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_TOP] ); - } -} diff --git a/src/widgets/control-center/control-details.h b/src/widgets/control-center/control-details.h deleted file mode 100644 index ba7b5ec..0000000 --- a/src/widgets/control-center/control-details.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_CONTROL_DETAILS foobar_control_details_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarControlDetails, foobar_control_details, FOOBAR, CONTROL_DETAILS, GtkWidget ) - -GtkWidget* foobar_control_details_new ( void ); -GtkWidget* foobar_control_details_get_child ( FoobarControlDetails* self ); -gboolean foobar_control_details_is_expanded ( FoobarControlDetails* self ); -gint foobar_control_details_get_inset_top( FoobarControlDetails* self ); -void foobar_control_details_set_child ( FoobarControlDetails* self, - GtkWidget* value ); -void foobar_control_details_set_expanded ( FoobarControlDetails* self, - gboolean value ); -void foobar_control_details_set_inset_top( FoobarControlDetails* self, - gint value ); - -G_END_DECLS diff --git a/src/widgets/control-center/control-slider.c b/src/widgets/control-center/control-slider.c deleted file mode 100644 index 9c69190..0000000 --- a/src/widgets/control-center/control-slider.c +++ /dev/null @@ -1,428 +0,0 @@ -#include "widgets/control-center/control-slider.h" - -// -// FoobarControlSlider: -// -// A slider in the "controls" section of the control center with a title and icon, as well as an optional secondary -// button to toggle a details view. -// - -struct _FoobarControlSlider -{ - GtkWidget parent_instance; - gchar* icon_name; - gchar* label; - gboolean can_expand; - gboolean is_expanded; - gint percentage; -}; - -enum -{ - PROP_ICON_NAME = 1, - PROP_LABEL, - PROP_CAN_EXPAND, - PROP_IS_EXPANDED, - PROP_PERCENTAGE, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_control_slider_class_init ( FoobarControlSliderClass* klass ); -static void foobar_control_slider_init ( FoobarControlSlider* self ); -static void foobar_control_slider_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_control_slider_set_property ( GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ); -static void foobar_control_slider_dispose ( GObject* object ); -static void foobar_control_slider_finalize ( GObject* object ); -static void foobar_control_slider_handle_expand_clicked( GtkButton* button, - gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarControlSlider, foobar_control_slider, GTK_TYPE_WIDGET ) - -// --------------------------------------------------------------------------------------------------------------------- -// Widget Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for control sliders. -// -void foobar_control_slider_class_init( FoobarControlSliderClass* klass ) -{ - GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); - gtk_widget_class_set_css_name( widget_klass, "control-slider" ); - gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT ); - - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_control_slider_get_property; - object_klass->set_property = foobar_control_slider_set_property; - object_klass->dispose = foobar_control_slider_dispose; - object_klass->finalize = foobar_control_slider_finalize; - - props[PROP_ICON_NAME] = g_param_spec_string( - "icon-name", - "Icon Name", - "Name of the icon to be displayed next to the label.", - NULL, - G_PARAM_READWRITE ); - props[PROP_LABEL] = g_param_spec_string( - "label", - "Label", - "The slider's main label.", - NULL, - G_PARAM_READWRITE ); - props[PROP_CAN_EXPAND] = g_param_spec_boolean( - "can-expand", - "Can Expand", - "Indicates whether some details are hidden and can be revealed by a toggle button next to the slider.", - FALSE, - G_PARAM_READWRITE ); - props[PROP_IS_EXPANDED] = g_param_spec_boolean( - "is-expanded", - "Is Expanded", - "Indicates whether additional details should be shown.", - FALSE, - G_PARAM_READWRITE ); - props[PROP_PERCENTAGE] = g_param_spec_int( - "percentage", - "Percentage", - "Value shown by the slider.", - 0, - 100, - 0, - G_PARAM_READWRITE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for control sliders. -// -void foobar_control_slider_init( FoobarControlSlider* self ) -{ - GtkWidget* icon = gtk_image_new( ); - gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); - g_object_bind_property( - self, - "icon-name", - icon, - "icon-name", - G_BINDING_SYNC_CREATE ); - - GtkWidget* label = gtk_label_new( NULL ); - gtk_widget_set_valign( label, GTK_ALIGN_CENTER ); - gtk_widget_set_hexpand( label, TRUE ); - gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT ); - gtk_label_set_xalign( GTK_LABEL( label ), 0 ); - g_object_bind_property( - self, - "label", - label, - "label", - G_BINDING_SYNC_CREATE ); - - GtkWidget* header = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); - gtk_box_append( GTK_BOX( header ), icon ); - gtk_box_append( GTK_BOX( header ), label ); - - GtkWidget* scale = gtk_scale_new_with_range( GTK_ORIENTATION_HORIZONTAL, 0, 100, 2 ); - g_object_bind_property( - self, - "percentage", - gtk_range_get_adjustment( GTK_RANGE( scale ) ), - "value", - G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL ); - - GtkWidget* primary_content = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); - gtk_widget_add_css_class( primary_content, "primary" ); - gtk_box_append( GTK_BOX( primary_content ), header ); - gtk_box_append( GTK_BOX( primary_content ), scale ); - - GtkWidget* expand_button = gtk_button_new_from_icon_name( "fluent-chevron-right-symbolic" ); - gtk_widget_add_css_class( expand_button, "expand" ); - g_object_bind_property( - self, - "can-expand", - expand_button, - "visible", - G_BINDING_SYNC_CREATE ); - g_signal_connect( expand_button, "clicked", G_CALLBACK( foobar_control_slider_handle_expand_clicked ), self ); - - GtkLayoutManager* layout = gtk_widget_get_layout_manager( GTK_WIDGET( self ) ); - gtk_orientable_set_orientation( GTK_ORIENTABLE( layout ), GTK_ORIENTATION_HORIZONTAL ); - gtk_widget_insert_before( primary_content, GTK_WIDGET( self ), NULL ); - gtk_widget_insert_before( expand_button, GTK_WIDGET( self ), NULL ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_control_slider_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarControlSlider* self = (FoobarControlSlider*)object; - - switch ( prop_id ) - { - case PROP_ICON_NAME: - g_value_set_string( value, foobar_control_slider_get_icon_name( self ) ); - break; - case PROP_LABEL: - g_value_set_string( value, foobar_control_slider_get_label( self ) ); - break; - case PROP_CAN_EXPAND: - g_value_set_boolean( value, foobar_control_slider_can_expand( self ) ); - break; - case PROP_IS_EXPANDED: - g_value_set_boolean( value, foobar_control_slider_is_expanded( self ) ); - break; - case PROP_PERCENTAGE: - g_value_set_int( value, foobar_control_slider_get_percentage( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Property setter implementation, mapping a property id to a method. -// -void foobar_control_slider_set_property( - GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ) -{ - FoobarControlSlider* self = (FoobarControlSlider*)object; - - switch ( prop_id ) - { - case PROP_ICON_NAME: - foobar_control_slider_set_icon_name( self, g_value_get_string( value ) ); - break; - case PROP_LABEL: - foobar_control_slider_set_label( self, g_value_get_string( value ) ); - break; - case PROP_CAN_EXPAND: - foobar_control_slider_set_can_expand( self, g_value_get_boolean( value ) ); - break; - case PROP_IS_EXPANDED: - foobar_control_slider_set_expanded( self, g_value_get_boolean( value ) ); - break; - case PROP_PERCENTAGE: - foobar_control_slider_set_percentage( self, g_value_get_int( value ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance de-initialization for control sliders. -// -void foobar_control_slider_dispose( GObject* object ) -{ - FoobarControlSlider* self = (FoobarControlSlider*)object; - - GtkWidget* child; - while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) - { - gtk_widget_unparent( child ); - } - - G_OBJECT_CLASS( foobar_control_slider_parent_class )->dispose( object ); -} - -// -// Instance cleanup for control sliders. -// -void foobar_control_slider_finalize( GObject* object ) -{ - FoobarControlSlider* self = (FoobarControlSlider*)object; - - g_clear_pointer( &self->icon_name, g_free ); - g_clear_pointer( &self->label, g_free ); - - G_OBJECT_CLASS( foobar_control_slider_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new control slider instance. -// -GtkWidget* foobar_control_slider_new( void ) -{ - return g_object_new( FOOBAR_TYPE_CONTROL_SLIDER, NULL ); -} - -// -// Get the name of the icon to be displayed next to the label. -// -gchar const* foobar_control_slider_get_icon_name( FoobarControlSlider* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), NULL ); - return self->icon_name; -} - -// -// Get the slider's main label. -// -gchar const* foobar_control_slider_get_label( FoobarControlSlider* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), NULL ); - return self->label; -} - -// -// Check whether some details are hidden and can be revealed by a toggle button next to the slider. -// -gboolean foobar_control_slider_can_expand( FoobarControlSlider* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE ); - return self->can_expand; -} - -// -// Check whether additional details should currently be shown. -// -gboolean foobar_control_slider_is_expanded( FoobarControlSlider* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE ); - return self->is_expanded; -} - -// -// Get the value shown by the slider. -// -gint foobar_control_slider_get_percentage( FoobarControlSlider* self ) -{ - g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE ); - return self->percentage; -} - -// -// Update the name of the icon to be displayed next to the label. -// -void foobar_control_slider_set_icon_name( - FoobarControlSlider* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); - - if ( g_strcmp0( self->icon_name, value ) ) - { - g_clear_pointer( &self->icon_name, g_free ); - self->icon_name = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ICON_NAME] ); - } -} - -// -// Update the slider's main label. -// -void foobar_control_slider_set_label( - FoobarControlSlider* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); - - if ( g_strcmp0( self->label, value ) ) - { - g_clear_pointer( &self->label, g_free ); - self->label = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] ); - } -} - -// -// Update whether some details are hidden and can be revealed by a toggle button next to the slider. -// -void foobar_control_slider_set_can_expand( - FoobarControlSlider* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); - - if ( !value ) { foobar_control_slider_set_expanded( self, FALSE ); } - - value = !!value; - if ( self->can_expand != value ) - { - self->can_expand = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_EXPAND] ); - } -} - -// -// Update whether additional details are currently shown. -// -void foobar_control_slider_set_expanded( - FoobarControlSlider* self, - gboolean value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); - - value = value && self->can_expand; - if ( self->is_expanded != value ) - { - self->is_expanded = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] ); - - if ( self->is_expanded ) - { - gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" ); - } - else - { - gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" ); - } - } -} - -// -// Update the value shown by the slider. -// -void foobar_control_slider_set_percentage( FoobarControlSlider* self, gint value ) -{ - g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) ); - - value = CLAMP( value, 0, 100 ); - if ( self->percentage != value ) - { - self->percentage = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_PERCENTAGE] ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called when the expand button was clicked. -// -// If allowed, this will toggle the "expanded" state of the slider. -// -void foobar_control_slider_handle_expand_clicked( GtkButton* button, gpointer userdata ) -{ - (void)button; - FoobarControlSlider* self = (FoobarControlSlider*)userdata; - - if ( foobar_control_slider_can_expand( self ) ) - { - foobar_control_slider_set_expanded( self, !foobar_control_slider_is_expanded( self ) ); - } -} diff --git a/src/widgets/control-center/control-slider.h b/src/widgets/control-center/control-slider.h deleted file mode 100644 index f797b5c..0000000 --- a/src/widgets/control-center/control-slider.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_CONTROL_SLIDER foobar_control_slider_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarControlSlider, foobar_control_slider, FOOBAR, CONTROL_SLIDER, GtkWidget ) - -GtkWidget* foobar_control_slider_new ( void ); -gchar const* foobar_control_slider_get_icon_name ( FoobarControlSlider* self ); -gchar const* foobar_control_slider_get_label ( FoobarControlSlider* self ); -gboolean foobar_control_slider_can_expand ( FoobarControlSlider* self ); -gboolean foobar_control_slider_is_expanded ( FoobarControlSlider* self ); -gint foobar_control_slider_get_percentage( FoobarControlSlider* self ); -void foobar_control_slider_set_icon_name ( FoobarControlSlider* self, - gchar const* value ); -void foobar_control_slider_set_label ( FoobarControlSlider* self, - gchar const* value ); -void foobar_control_slider_set_can_expand( FoobarControlSlider* self, - gboolean value ); -void foobar_control_slider_set_expanded ( FoobarControlSlider* self, - gboolean value ); -void foobar_control_slider_set_percentage( FoobarControlSlider* self, - gint value ); - -G_END_DECLS diff --git a/src/widgets/control-center/meson.build b/src/widgets/control-center/meson.build deleted file mode 100644 index 2cdc9ca..0000000 --- a/src/widgets/control-center/meson.build +++ /dev/null @@ -1,6 +0,0 @@ -foobar_sources += files( - 'control-button.c', - 'control-slider.c', - 'control-details.c', - 'control-details-item.c', -) diff --git a/src/widgets/inset-container.c b/src/widgets/inset-container.c deleted file mode 100644 index 052f872..0000000 --- a/src/widgets/inset-container.c +++ /dev/null @@ -1,363 +0,0 @@ -#include "widgets/inset-container.h" - -// -// FoobarInsetContainer: -// -// A custom widget which renders a child with an inset/padding. -// -// Notably, it also supports negative inset values which is useful in order to provide uniform spacing between widgets. -// - -struct _FoobarInsetContainer -{ - GtkWidget parent_instance; - GtkWidget* child; - gint inset_horizontal; - gint inset_vertical; -}; - -enum -{ - PROP_CHILD = 1, - PROP_INSET_HORIZONTAL, - PROP_INSET_VERTICAL, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_inset_container_class_init ( FoobarInsetContainerClass* klass ); -static void foobar_inset_container_init ( FoobarInsetContainer* self ); -static void foobar_inset_container_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_inset_container_set_property ( GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ); -static void foobar_inset_container_dispose ( GObject* object ); -static GtkSizeRequestMode foobar_inset_container_get_request_mode( GtkWidget* widget ); -static void foobar_inset_container_measure ( GtkWidget* widget, - GtkOrientation orientation, - int for_size, - int* minimum, - int* natural, - int* minimum_baseline, - int* natural_baseline ); -static void foobar_inset_container_size_allocate ( GtkWidget* widget, - int width, - int height, - int baseline ); - -G_DEFINE_FINAL_TYPE( FoobarInsetContainer, foobar_inset_container, GTK_TYPE_WIDGET ) - -// --------------------------------------------------------------------------------------------------------------------- -// Widget Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for inset containers. -// -void foobar_inset_container_class_init( FoobarInsetContainerClass* klass ) -{ - GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); - widget_klass->get_request_mode = foobar_inset_container_get_request_mode; - widget_klass->measure = foobar_inset_container_measure; - widget_klass->size_allocate = foobar_inset_container_size_allocate; - gtk_widget_class_set_css_name( widget_klass, "inset-container" ); - - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_inset_container_get_property; - object_klass->set_property = foobar_inset_container_set_property; - object_klass->dispose = foobar_inset_container_dispose; - - props[PROP_CHILD] = g_param_spec_object( - "child", - "Child", - "Child widget for the panel item.", - GTK_TYPE_WIDGET, - G_PARAM_READWRITE ); - props[PROP_INSET_HORIZONTAL] = g_param_spec_int( - "inset-horizontal", - "Inset Horizontal", - "The horizontal offset (may be negative).", - INT_MIN, - INT_MAX, - 0, - G_PARAM_READWRITE ); - props[PROP_INSET_VERTICAL] = g_param_spec_int( - "inset-vertical", - "Inset Vertical", - "The vertical offset (may be negative).", - INT_MIN, - INT_MAX, - 0, - G_PARAM_READWRITE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for inset containers. -// -void foobar_inset_container_init( FoobarInsetContainer* self ) -{ - (void)self; -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_inset_container_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarInsetContainer* self = (FoobarInsetContainer*)object; - - switch ( prop_id ) - { - case PROP_CHILD: - g_value_set_object( value, foobar_inset_container_get_child( self ) ); - break; - case PROP_INSET_HORIZONTAL: - g_value_set_int( value, foobar_inset_container_get_inset_horizontal( self ) ); - break; - case PROP_INSET_VERTICAL: - g_value_set_int( value, foobar_inset_container_get_inset_vertical( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Property setter implementation, mapping a property id to a method. -// -void foobar_inset_container_set_property( - GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ) -{ - FoobarInsetContainer* self = (FoobarInsetContainer*)object; - - switch ( prop_id ) - { - case PROP_CHILD: - foobar_inset_container_set_child( self, g_value_get_object( value ) ); - break; - case PROP_INSET_HORIZONTAL: - foobar_inset_container_set_inset_horizontal( self, g_value_get_int( value ) ); - break; - case PROP_INSET_VERTICAL: - foobar_inset_container_set_inset_vertical( self, g_value_get_int( value ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance de-initialization for inset containers. -// -void foobar_inset_container_dispose( GObject* object ) -{ - FoobarInsetContainer* self = (FoobarInsetContainer*)object; - - g_clear_pointer( &self->child, gtk_widget_unparent ); - - G_OBJECT_CLASS( foobar_inset_container_parent_class )->dispose( object ); -} - -// -// Determine the size request mode used to measure the widget. -// -// This implementation just forwards the request mode from the child widget. -// -GtkSizeRequestMode foobar_inset_container_get_request_mode( GtkWidget* widget ) -{ - FoobarInsetContainer* self = (FoobarInsetContainer*)widget; - if ( !self->child ) { return GTK_SIZE_REQUEST_CONSTANT_SIZE; } - - return gtk_widget_get_request_mode( self->child ); -} - -// -// Compute the requested size of the widget. -// -// This implementation uses the size requested by the child widget and then adds the inset to it. -// -void foobar_inset_container_measure( - GtkWidget* widget, - GtkOrientation orientation, - int for_size, - int* minimum, - int* natural, - int* minimum_baseline, - int* natural_baseline ) -{ - FoobarInsetContainer* self = (FoobarInsetContainer*)widget; - if ( !self->child ) - { - *minimum = 0; - *natural = 0; - *minimum_baseline = 0; - *natural_baseline = 0; - return; - } - - // Get the inset for the given orientation and the other one. - - gint inset; - gint other_inset; - switch ( orientation ) - { - case GTK_ORIENTATION_HORIZONTAL: - inset = self->inset_horizontal; - other_inset = self->inset_vertical; - break; - case GTK_ORIENTATION_VERTICAL: - inset = self->inset_vertical; - other_inset = self->inset_horizontal; - break; - default: - g_warn_if_reached( ); - return; - } - - // Compute the new size, taking into consideration that the inset might have already been added to measure the other - // side and needs to be subtracted before passing it to the child widget's measure function. - - for_size = MAX( 0, for_size - ( 2 * other_inset ) ); - gtk_widget_measure( self->child, orientation, for_size, minimum, natural, minimum_baseline, natural_baseline ); - *minimum = MAX( 0, *minimum + ( 2 * inset ) ); - *natural = MAX( 0, *natural + ( 2 * inset ) ); - if ( orientation == GTK_ORIENTATION_VERTICAL ) - { - *minimum_baseline = MAX( 0, *minimum_baseline + inset ); - *natural_baseline = MAX( 0, *natural_baseline + inset ); - } -} - -// -// Arrange the child widget. -// -void foobar_inset_container_size_allocate( - GtkWidget* widget, - int width, - int height, - int baseline ) -{ - FoobarInsetContainer* self = (FoobarInsetContainer*)widget; - if ( !self->child ) { return; } - - GtkAllocation allocation = - { - .width = MAX( 0, width - 2 * self->inset_horizontal ), - .height = MAX( 0, height - 2 * self->inset_vertical ), - .x = self->inset_horizontal, - .y = self->inset_vertical, - }; - gtk_widget_size_allocate( self->child, &allocation, MAX( 0, baseline + self->inset_vertical ) ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new inset container instance. -// -GtkWidget* foobar_inset_container_new( void ) -{ - return g_object_new( FOOBAR_TYPE_INSET_CONTAINER, NULL ); -} - -// -// Get the container's child widget. -// -GtkWidget* foobar_inset_container_get_child( FoobarInsetContainer* self ) -{ - g_return_val_if_fail( FOOBAR_IS_INSET_CONTAINER( self ), NULL ); - return self->child; -} - -// -// Get the inset applied to the left and right side of the widget. -// -gint foobar_inset_container_get_inset_horizontal( FoobarInsetContainer* self ) -{ - g_return_val_if_fail( FOOBAR_IS_INSET_CONTAINER( self ), 0 ); - return self->inset_horizontal; -} - -// -// Get the inset applied to the top and bottom side of the widget. -// -gint foobar_inset_container_get_inset_vertical( FoobarInsetContainer* self ) -{ - g_return_val_if_fail( FOOBAR_IS_INSET_CONTAINER( self ), 0 ); - return self->inset_vertical; -} - -// -// Update the container's child widget. -// -void foobar_inset_container_set_child( - FoobarInsetContainer* self, - GtkWidget* child ) -{ - g_return_if_fail( FOOBAR_IS_INSET_CONTAINER( self ) ); - g_return_if_fail( child == NULL || self->child == child || gtk_widget_get_parent( child ) == NULL ); - - if ( self->child != child ) - { - g_clear_pointer( &self->child, gtk_widget_unparent ); - - if ( child ) - { - self->child = child; - gtk_widget_set_parent( child, GTK_WIDGET( self ) ); - } - - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] ); - } -} - -// -// Update the inset applied to the left and right side of the widget. -// -void foobar_inset_container_set_inset_horizontal( - FoobarInsetContainer* self, - gint value ) -{ - g_return_if_fail( FOOBAR_IS_INSET_CONTAINER( self ) ); - - if ( self->inset_horizontal != value ) - { - self->inset_horizontal = value; - gtk_widget_queue_resize( GTK_WIDGET( self ) ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_HORIZONTAL] ); - } -} - -// -// Update the inset applied to the top and bottom side of the widget. -// -void foobar_inset_container_set_inset_vertical( - FoobarInsetContainer* self, - gint value ) -{ - g_return_if_fail( FOOBAR_IS_INSET_CONTAINER( self ) ); - - if ( self->inset_vertical != value ) - { - self->inset_vertical = value; - gtk_widget_queue_resize( GTK_WIDGET( self ) ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_VERTICAL] ); - } -} diff --git a/src/widgets/inset-container.h b/src/widgets/inset-container.h deleted file mode 100644 index 316d2be..0000000 --- a/src/widgets/inset-container.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_INSET_CONTAINER foobar_inset_container_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarInsetContainer, foobar_inset_container, FOOBAR, INSET_CONTAINER, GtkWidget ) - -GtkWidget* foobar_inset_container_new ( void ); -GtkWidget* foobar_inset_container_get_child ( FoobarInsetContainer* self ); -gint foobar_inset_container_get_inset_horizontal( FoobarInsetContainer* self ); -gint foobar_inset_container_get_inset_vertical ( FoobarInsetContainer* self ); -void foobar_inset_container_set_child ( FoobarInsetContainer* self, - GtkWidget* value ); -void foobar_inset_container_set_inset_horizontal( FoobarInsetContainer* self, - gint value ); -void foobar_inset_container_set_inset_vertical ( FoobarInsetContainer* self, - gint value ); - -G_END_DECLS diff --git a/src/widgets/limit-container.c b/src/widgets/limit-container.c deleted file mode 100644 index 123acef..0000000 --- a/src/widgets/limit-container.c +++ /dev/null @@ -1,341 +0,0 @@ -#include "widgets/limit-container.h" - -// -// FoobarLimitContainer: -// -// A container which uses the natural size requested by the child as its minimum size, but only up to a certain point. -// -// This is useful because gtk_scrolled_window_set_max_content_height doesn't seem to be doing what I understand it to -// do. This way, we can set propagate_natural_height to TRUE and then limit the height through this container. -// - -struct _FoobarLimitContainer -{ - GtkWidget parent_instance; - GtkWidget* child; - gint max_width; - gint max_height; -}; - -enum -{ - PROP_CHILD = 1, - PROP_MAX_WIDTH, - PROP_MAX_HEIGHT, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_limit_container_class_init ( FoobarLimitContainerClass* klass ); -static void foobar_limit_container_init ( FoobarLimitContainer* self ); -static void foobar_limit_container_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_limit_container_set_property ( GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ); -static void foobar_limit_container_dispose ( GObject* object ); -static GtkSizeRequestMode foobar_limit_container_get_request_mode( GtkWidget* widget ); -static void foobar_limit_container_measure ( GtkWidget* widget, - GtkOrientation orientation, - int for_size, - int* minimum, - int* natural, - int* minimum_baseline, - int* natural_baseline ); -static void foobar_limit_container_size_allocate ( GtkWidget* widget, - int width, - int height, - int baseline ); - -G_DEFINE_FINAL_TYPE( FoobarLimitContainer, foobar_limit_container, GTK_TYPE_WIDGET ) - -// --------------------------------------------------------------------------------------------------------------------- -// Widget Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for limit containers. -// -void foobar_limit_container_class_init( FoobarLimitContainerClass* klass ) -{ - GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); - widget_klass->get_request_mode = foobar_limit_container_get_request_mode; - widget_klass->measure = foobar_limit_container_measure; - widget_klass->size_allocate = foobar_limit_container_size_allocate; - gtk_widget_class_set_css_name( widget_klass, "limit-container" ); - - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_limit_container_get_property; - object_klass->set_property = foobar_limit_container_set_property; - object_klass->dispose = foobar_limit_container_dispose; - - props[PROP_CHILD] = g_param_spec_object( - "child", - "Child", - "Child widget for the panel item.", - GTK_TYPE_WIDGET, - G_PARAM_READWRITE ); - props[PROP_MAX_WIDTH] = g_param_spec_int( - "max-width", - "Max Width", - "The maximum natural width supported as a minimum size.", - 0, - INT_MAX, - 0, - G_PARAM_READWRITE ); - props[PROP_MAX_HEIGHT] = g_param_spec_int( - "max-height", - "Max Height", - "The maximum natural height supported as a minimum size.", - 0, - INT_MAX, - 0, - G_PARAM_READWRITE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for limit containers. -// -void foobar_limit_container_init( FoobarLimitContainer* self ) -{ - (void)self; -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_limit_container_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarLimitContainer* self = (FoobarLimitContainer*)object; - - switch ( prop_id ) - { - case PROP_CHILD: - g_value_set_object( value, foobar_limit_container_get_child( self ) ); - break; - case PROP_MAX_WIDTH: - g_value_set_int( value, foobar_limit_container_get_max_width( self ) ); - break; - case PROP_MAX_HEIGHT: - g_value_set_int( value, foobar_limit_container_get_max_height( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Property setter implementation, mapping a property id to a method. -// -void foobar_limit_container_set_property( - GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ) -{ - FoobarLimitContainer* self = (FoobarLimitContainer*)object; - - switch ( prop_id ) - { - case PROP_CHILD: - foobar_limit_container_set_child( self, g_value_get_object( value ) ); - break; - case PROP_MAX_WIDTH: - foobar_limit_container_set_max_width( self, g_value_get_int( value ) ); - break; - case PROP_MAX_HEIGHT: - foobar_limit_container_set_max_height( self, g_value_get_int( value ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance de-initialization for limit containers. -// -void foobar_limit_container_dispose( GObject* object ) -{ - FoobarLimitContainer* self = (FoobarLimitContainer*)object; - - g_clear_pointer( &self->child, gtk_widget_unparent ); - - G_OBJECT_CLASS( foobar_limit_container_parent_class )->dispose( object ); -} - -// -// Determine the size request mode used to measure the widget. -// -// This implementation just forwards the request mode from the child widget. -// -GtkSizeRequestMode foobar_limit_container_get_request_mode( GtkWidget* widget ) -{ - FoobarLimitContainer* self = (FoobarLimitContainer*)widget; - if ( !self->child ) { return GTK_SIZE_REQUEST_CONSTANT_SIZE; } - - return gtk_widget_get_request_mode( self->child ); -} - -// -// Compute the requested size of the widget. -// -void foobar_limit_container_measure( - GtkWidget* widget, - GtkOrientation orientation, - int for_size, - int* minimum, - int* natural, - int* minimum_baseline, - int* natural_baseline ) -{ - FoobarLimitContainer* self = (FoobarLimitContainer*)widget; - if ( !self->child ) - { - *minimum = 0; - *natural = 0; - *minimum_baseline = 0; - *natural_baseline = 0; - return; - } - - gtk_widget_measure( self->child, orientation, for_size, minimum, natural, minimum_baseline, natural_baseline ); - gint natural_minimum = *natural; - switch ( orientation ) - { - case GTK_ORIENTATION_HORIZONTAL: - natural_minimum = MIN( natural_minimum, self->max_width ); - break; - case GTK_ORIENTATION_VERTICAL: - natural_minimum = MIN( natural_minimum, self->max_height ); - break; - default: - g_warn_if_reached( ); - break; - } - *minimum = MAX( *minimum, natural_minimum ); -} - -// -// Arrange the child widget. -// -void foobar_limit_container_size_allocate( - GtkWidget* widget, - int width, - int height, - int baseline ) -{ - FoobarLimitContainer* self = (FoobarLimitContainer*)widget; - if ( !self->child ) { return; } - - gtk_widget_allocate( self->child, width, height, baseline, NULL ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new limit container instance. -// -GtkWidget* foobar_limit_container_new( void ) -{ - return g_object_new( FOOBAR_TYPE_LIMIT_CONTAINER, NULL ); -} - -// -// Get the container's child widget. -// -GtkWidget* foobar_limit_container_get_child( FoobarLimitContainer* self ) -{ - g_return_val_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ), NULL ); - return self->child; -} - -// -// Get the maximum natural width of the child to use as the minimum width for this container. -// -gint foobar_limit_container_get_max_width( FoobarLimitContainer* self ) -{ - g_return_val_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ), 0 ); - return self->max_width; -} - -// -// Get the maximum natural height of the child to use as the minimum height for this container. -// -gint foobar_limit_container_get_max_height( FoobarLimitContainer* self ) -{ - g_return_val_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ), 0 ); - return self->max_height; -} - -// -// Update the container's child widget. -// -void foobar_limit_container_set_child( - FoobarLimitContainer* self, - GtkWidget* child ) -{ - g_return_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ) ); - g_return_if_fail( child == NULL || self->child == child || gtk_widget_get_parent( child ) == NULL ); - - if ( self->child != child ) - { - g_clear_pointer( &self->child, gtk_widget_unparent ); - - if ( child ) - { - self->child = child; - gtk_widget_set_parent( child, GTK_WIDGET( self ) ); - } - - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] ); - } -} - -// -// Update the maximum natural width of the child to use as the minimum width for this container. -// -void foobar_limit_container_set_max_width( - FoobarLimitContainer* self, - gint value ) -{ - g_return_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ) ); - g_return_if_fail( value >= 0 ); - - if ( self->max_width != value ) - { - self->max_width = value; - gtk_widget_queue_resize( GTK_WIDGET( self ) ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_MAX_WIDTH] ); - } -} - -// -// Update the maximum natural height of the child to use as the minimum height for this container. -// -void foobar_limit_container_set_max_height( - FoobarLimitContainer* self, - gint value ) -{ - g_return_if_fail( FOOBAR_IS_LIMIT_CONTAINER( self ) ); - g_return_if_fail( value >= 0 ); - - if ( self->max_height != value ) - { - self->max_height = value; - gtk_widget_queue_resize( GTK_WIDGET( self ) ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_MAX_HEIGHT] ); - } -} diff --git a/src/widgets/limit-container.h b/src/widgets/limit-container.h deleted file mode 100644 index cad19aa..0000000 --- a/src/widgets/limit-container.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_LIMIT_CONTAINER foobar_limit_container_get_type( ) - -G_DECLARE_FINAL_TYPE( FoobarLimitContainer, foobar_limit_container, FOOBAR, LIMIT_CONTAINER, GtkWidget ) - -GtkWidget* foobar_limit_container_new ( void ); -GtkWidget* foobar_limit_container_get_child ( FoobarLimitContainer* self ); -gint foobar_limit_container_get_max_width ( FoobarLimitContainer* self ); -gint foobar_limit_container_get_max_height( FoobarLimitContainer* self ); -void foobar_limit_container_set_child ( FoobarLimitContainer* self, - GtkWidget* value ); -void foobar_limit_container_set_max_width ( FoobarLimitContainer* self, - gint value ); -void foobar_limit_container_set_max_height( FoobarLimitContainer* self, - gint value ); - -G_END_DECLS diff --git a/src/widgets/meson.build b/src/widgets/meson.build deleted file mode 100644 index 7aeb3ca..0000000 --- a/src/widgets/meson.build +++ /dev/null @@ -1,8 +0,0 @@ -foobar_sources += files( - 'inset-container.c', - 'limit-container.c', - 'notification-widget.c', -) - -subdir('control-center') -subdir('panel') diff --git a/src/widgets/notification-widget.c b/src/widgets/notification-widget.c deleted file mode 100644 index 97cfc22..0000000 --- a/src/widgets/notification-widget.c +++ /dev/null @@ -1,821 +0,0 @@ -#include "widgets/notification-widget.h" - -// -// FoobarNotificationCloseAction: -// -// The action invoked when the user requests to "close" the notification. The notification is either marked as -// "dismissed" and kept around or removed from the list. -// - -G_DEFINE_ENUM_TYPE( - FoobarNotificationCloseAction, - foobar_notification_close_action, - G_DEFINE_ENUM_VALUE( FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE, "remove" ), - G_DEFINE_ENUM_VALUE( FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS, "dismiss" ) ) - -// -// FoobarNotificationWidget: -// -// Shared widget for displaying a notification. The widget also manages an inset/margin -- this is done to allow the -// "close" button to extend into the margin area. -// - -struct _FoobarNotificationWidget -{ - GtkWidget parent_instance; - GtkWidget* container; - GtkWidget* content; - GtkWidget* close_button; - FoobarNotification* notification; - FoobarNotificationCloseAction close_action; - gchar* time_format; - gint min_height; - gint close_button_inset; - gint inset_start; - gint inset_end; - gint inset_top; - gint inset_bottom; -}; - -enum -{ - PROP_NOTIFICATION = 1, - PROP_CLOSE_ACTION, - PROP_TIME_FORMAT, - PROP_MIN_HEIGHT, - PROP_CLOSE_BUTTON_INSET, - PROP_INSET_START, - PROP_INSET_END, - PROP_INSET_TOP, - PROP_INSET_BOTTOM, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; - -static void foobar_notification_widget_class_init ( FoobarNotificationWidgetClass* klass ); -static void foobar_notification_widget_init ( FoobarNotificationWidget* self ); -static void foobar_notification_widget_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_notification_widget_set_property ( GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ); -static void foobar_notification_widget_dispose ( GObject* object ); -static void foobar_notification_widget_finalize ( GObject* object ); -static void foobar_notification_widget_handle_enter ( GtkEventControllerMotion* controller, - gdouble x, - gdouble y, - gpointer userdata ); -static void foobar_notification_widget_handle_leave ( GtkEventControllerMotion* controller, - gpointer userdata ); -static void foobar_notification_widget_handle_close_clicked( GtkButton* button, - gpointer userdata ); -static gchar* foobar_notification_widget_compute_time_label ( GtkExpression* expression, - GDateTime* time, - gchar const* format, - gpointer userdata ); -static gboolean foobar_notification_widget_compute_icon_visible( GtkExpression* expression, - GIcon* icon, - gpointer userdata ); -static gboolean foobar_notification_widget_compute_body_visible( GtkExpression* expression, - gchar const* body, - gpointer userdata ); -static void foobar_notification_widget_update_margins ( FoobarNotificationWidget* self ); - -G_DEFINE_FINAL_TYPE( FoobarNotificationWidget, foobar_notification_widget, GTK_TYPE_WIDGET ) - -// --------------------------------------------------------------------------------------------------------------------- -// Widget Implementation -// --------------------------------------------------------------------------------------------------------------------- - -// -// Static initialization for notifications. -// -void foobar_notification_widget_class_init( FoobarNotificationWidgetClass* klass ) -{ - GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass ); - gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BIN_LAYOUT ); - gtk_widget_class_set_css_name( widget_klass, "notification" ); - - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_notification_widget_get_property; - object_klass->set_property = foobar_notification_widget_set_property; - object_klass->dispose = foobar_notification_widget_dispose; - object_klass->finalize = foobar_notification_widget_finalize; - - props[PROP_NOTIFICATION] = g_param_spec_object( - "notification", - "Notification", - "Content to display.", - FOOBAR_TYPE_NOTIFICATION, - G_PARAM_READWRITE ); - props[PROP_CLOSE_ACTION] = g_param_spec_enum( - "close-action", - "Close Action", - "Action to execute when the notification is closed.", - FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION, - 0, - G_PARAM_READWRITE ); - props[PROP_TIME_FORMAT] = g_param_spec_string( - "time-format", - "Time Format", - "Time format used to display the notification's timestamp.", - NULL, - G_PARAM_READWRITE ); - props[PROP_MIN_HEIGHT] = g_param_spec_int( - "min-height", - "Minimum Height", - "Minimum requested height for the notification itself.", - 0, - INT_MAX, - 0, - G_PARAM_READWRITE ); - props[PROP_CLOSE_BUTTON_INSET] = g_param_spec_int( - "close-button-inset", - "Close Button Inset", - "Offset of the close button within the notification (may be negative, possibly increasing the actual inset).", - INT_MIN, - INT_MAX, - 0, - G_PARAM_READWRITE ); - props[PROP_INSET_START] = g_param_spec_int( - "inset-start", - "Inset Start", - "Inset for the start edge of the widget.", - 0, - INT_MAX, - 0, - G_PARAM_READWRITE ); - props[PROP_INSET_END] = g_param_spec_int( - "inset-end", - "Inset End", - "Inset for the end edge of the widget.", - 0, - INT_MAX, - 0, - G_PARAM_READWRITE ); - props[PROP_INSET_TOP] = g_param_spec_int( - "inset-top", - "Inset Top", - "Inset for the top edge of the widget.", - 0, - INT_MAX, - 0, - G_PARAM_READWRITE ); - props[PROP_INSET_BOTTOM] = g_param_spec_int( - "inset-bottom", - "Inset Bottom", - "Inset for the bottom edge of the widget.", - 0, - INT_MAX, - 0, - G_PARAM_READWRITE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); -} - -// -// Instance initialization for notifications. -// -void foobar_notification_widget_init( FoobarNotificationWidget* self ) -{ - // Set up the main notification content. - - GtkWidget* icon = gtk_image_new( ); - gtk_widget_add_css_class( icon, "icon" ); - gtk_widget_set_valign( icon, GTK_ALIGN_CENTER ); - - GtkWidget* title = gtk_label_new( NULL ); - gtk_label_set_justify( GTK_LABEL( title ), GTK_JUSTIFY_LEFT ); - gtk_label_set_ellipsize( GTK_LABEL( title ), PANGO_ELLIPSIZE_END ); - gtk_label_set_wrap( GTK_LABEL( title ), FALSE ); - gtk_label_set_xalign( GTK_LABEL( title ), 0 ); - gtk_widget_add_css_class( title, "title" ); - gtk_widget_set_valign( title, GTK_ALIGN_BASELINE_CENTER ); - gtk_widget_set_hexpand( title, TRUE ); - - GtkWidget* time = gtk_label_new( NULL ); - gtk_label_set_wrap( GTK_LABEL( time ), FALSE ); - gtk_widget_add_css_class( time, "time" ); - gtk_widget_set_valign( time, GTK_ALIGN_BASELINE_CENTER ); - - GtkWidget* body = gtk_label_new( NULL ); - gtk_label_set_use_markup( GTK_LABEL( body ), TRUE ); - gtk_label_set_justify( GTK_LABEL( body ), GTK_JUSTIFY_LEFT ); - gtk_label_set_ellipsize( GTK_LABEL( body ), PANGO_ELLIPSIZE_END ); - gtk_label_set_wrap( GTK_LABEL( body ), TRUE ); - gtk_label_set_wrap_mode( GTK_LABEL( body ), PANGO_WRAP_WORD_CHAR ); - // XXX: Increase once layout is fixed in GTK (https://gitlab.gnome.org/GNOME/gtk/-/issues/5885) - gtk_label_set_lines( GTK_LABEL( body ), 1 ); - gtk_label_set_xalign( GTK_LABEL( body ), 0 ); - gtk_widget_add_css_class( body, "body" ); - - GtkWidget* header = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); - gtk_box_append( GTK_BOX( header ), title ); - gtk_box_append( GTK_BOX( header ), time ); - - GtkWidget* row = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); - gtk_box_append( GTK_BOX( row ), header ); - gtk_box_append( GTK_BOX( row ), body ); - gtk_widget_set_hexpand( row, TRUE ); - gtk_widget_set_valign( row, GTK_ALIGN_CENTER ); - - self->content = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 ); - gtk_box_append( GTK_BOX( self->content ), icon ); - gtk_box_append( GTK_BOX( self->content ), row ); - gtk_widget_add_css_class( self->content, "content" ); - - // Set up the overlay for the close button. - - GtkWidget* close_icon = gtk_image_new_from_icon_name( "fluent-dismiss-symbolic" ); - gtk_image_set_pixel_size( GTK_IMAGE( close_icon ), 12 ); - - self->close_button = gtk_button_new( ); - gtk_button_set_child( GTK_BUTTON( self->close_button ), close_icon ); - gtk_widget_add_css_class( self->close_button, "close-button" ); - gtk_widget_set_visible( self->close_button, FALSE ); - gtk_widget_set_halign( self->close_button, GTK_ALIGN_END ); - gtk_widget_set_valign( self->close_button, GTK_ALIGN_START ); - g_signal_connect( self->close_button, "clicked", G_CALLBACK( foobar_notification_widget_handle_close_clicked ), self ); - - GtkEventController* hover_controller = gtk_event_controller_motion_new( ); - g_signal_connect( hover_controller, "enter", G_CALLBACK( foobar_notification_widget_handle_enter ), self ); - g_signal_connect( hover_controller, "leave", G_CALLBACK( foobar_notification_widget_handle_leave ), self ); - - self->container = gtk_overlay_new( ); - gtk_overlay_set_child( GTK_OVERLAY( self->container ), self->content ); - gtk_overlay_add_overlay( GTK_OVERLAY( self->container ), self->close_button ); - gtk_widget_add_controller( self->container, hover_controller ); - gtk_widget_set_parent( self->container, GTK_WIDGET( self ) ); - - // Set up bindings. - - { - GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" ); - GtkExpression* image_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "image" ); - gtk_expression_bind( gtk_expression_ref( image_expr ), icon, "gicon", self ); - GtkExpression* visible_params[] = { image_expr }; - GtkExpression* visible_expr = gtk_cclosure_expression_new( - G_TYPE_BOOLEAN, - NULL, - G_N_ELEMENTS( visible_params ), - visible_params, - G_CALLBACK( foobar_notification_widget_compute_icon_visible ), - NULL, - NULL ); - gtk_expression_bind( visible_expr, icon, "visible", self ); - } - - { - GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" ); - GtkExpression* summary_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "summary" ); - gtk_expression_bind( summary_expr, title, "label", self ); - } - - { - GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" ); - GtkExpression* body_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "body" ); - gtk_expression_bind( gtk_expression_ref( body_expr ), body, "label", self ); - GtkExpression* visible_params[] = { body_expr }; - GtkExpression* visible_expr = gtk_cclosure_expression_new( - G_TYPE_BOOLEAN, - NULL, - G_N_ELEMENTS( visible_params ), - visible_params, - G_CALLBACK( foobar_notification_widget_compute_body_visible ), - NULL, - NULL ); - gtk_expression_bind( visible_expr, body, "visible", self ); - } - - { - GtkExpression* notification_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "notification" ); - GtkExpression* time_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION, notification_expr, "time" ); - GtkExpression* format_expr = gtk_property_expression_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL, "time-format" ); - GtkExpression* str_params[] = { time_expr, format_expr }; - GtkExpression* str_expr = gtk_cclosure_expression_new( - G_TYPE_STRING, - NULL, - G_N_ELEMENTS( str_params ), - str_params, - G_CALLBACK( foobar_notification_widget_compute_time_label ), - self, - NULL ); - gtk_expression_bind( str_expr, time, "label", self ); - } -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_notification_widget_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarNotificationWidget* self = (FoobarNotificationWidget*)object; - - switch ( prop_id ) - { - case PROP_NOTIFICATION: - g_value_set_object( value, foobar_notification_widget_get_notification( self ) ); - break; - case PROP_CLOSE_ACTION: - g_value_set_enum( value, foobar_notification_widget_get_close_action( self ) ); - break; - case PROP_TIME_FORMAT: - g_value_set_string( value, foobar_notification_widget_get_time_format( self ) ); - break; - case PROP_MIN_HEIGHT: - g_value_set_int( value, foobar_notification_widget_get_min_height( self ) ); - break; - case PROP_CLOSE_BUTTON_INSET: - g_value_set_int( value, foobar_notification_widget_get_close_button_inset( self ) ); - break; - case PROP_INSET_START: - g_value_set_int( value, foobar_notification_widget_get_inset_start( self ) ); - break; - case PROP_INSET_END: - g_value_set_int( value, foobar_notification_widget_get_inset_end( self ) ); - break; - case PROP_INSET_TOP: - g_value_set_int( value, foobar_notification_widget_get_inset_top( self ) ); - break; - case PROP_INSET_BOTTOM: - g_value_set_int( value, foobar_notification_widget_get_inset_bottom( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Property setter implementation, mapping a property id to a method. -// -void foobar_notification_widget_set_property( - GObject* object, - guint prop_id, - GValue const* value, - GParamSpec* pspec ) -{ - FoobarNotificationWidget* self = (FoobarNotificationWidget*)object; - - switch ( prop_id ) - { - case PROP_NOTIFICATION: - foobar_notification_widget_set_notification( self, g_value_get_object( value ) ); - break; - case PROP_CLOSE_ACTION: - foobar_notification_widget_set_close_action( self, g_value_get_enum( value ) ); - break; - case PROP_TIME_FORMAT: - foobar_notification_widget_set_time_format( self, g_value_get_string( value ) ); - break; - case PROP_MIN_HEIGHT: - foobar_notification_widget_set_min_height( self, g_value_get_int( value ) ); - break; - case PROP_CLOSE_BUTTON_INSET: - foobar_notification_widget_set_close_button_inset( self, g_value_get_int( value ) ); - break; - case PROP_INSET_START: - foobar_notification_widget_set_inset_start( self, g_value_get_int( value ) ); - break; - case PROP_INSET_END: - foobar_notification_widget_set_inset_end( self, g_value_get_int( value ) ); - break; - case PROP_INSET_TOP: - foobar_notification_widget_set_inset_top( self, g_value_get_int( value ) ); - break; - case PROP_INSET_BOTTOM: - foobar_notification_widget_set_inset_bottom( self, g_value_get_int( value ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance de-initialization for notifications. -// -void foobar_notification_widget_dispose( GObject* object ) -{ - FoobarNotificationWidget* self = (FoobarNotificationWidget*)object; - - GtkWidget* child; - while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) ) - { - gtk_widget_unparent( child ); - } - - G_OBJECT_CLASS( foobar_notification_widget_parent_class )->dispose( object ); -} - -// -// Instance cleanup for notifications. -// -void foobar_notification_widget_finalize( GObject* object ) -{ - FoobarNotificationWidget* self = (FoobarNotificationWidget*)object; - - g_clear_object( &self->notification ); - g_clear_pointer( &self->time_format, g_free ); - - G_OBJECT_CLASS( foobar_notification_widget_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new notification widget instance. -// -GtkWidget* foobar_notification_widget_new( void ) -{ - return g_object_new( FOOBAR_TYPE_NOTIFICATION_WIDGET, NULL ); -} - -// -// Get the notification model currently displayed. -// -FoobarNotification* foobar_notification_widget_get_notification( FoobarNotificationWidget* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), NULL ); - return self->notification; -} - -// -// Get the action invoked when the user wants to "close" a notification. -// -FoobarNotificationCloseAction foobar_notification_widget_get_close_action( FoobarNotificationWidget* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); - return self->close_action; -} - -// -// Get the time format string used to display the notification's timestamp. -// -gchar const* foobar_notification_widget_get_time_format( FoobarNotificationWidget* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), NULL ); - return self->time_format; -} - -// -// Get the minimum height of the notification (excluding the inset). -// -gint foobar_notification_widget_get_min_height( FoobarNotificationWidget* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); - return self->min_height; -} - -// -// Get the offset of the close button within the notification (may be negative, possibly increasing the actual inset). -// -gint foobar_notification_widget_get_close_button_inset( FoobarNotificationWidget* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); - return self->close_button_inset; -} - -// -// Get the margin for the start edge of the notification. -// -gint foobar_notification_widget_get_inset_start( FoobarNotificationWidget* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); - return self->inset_start; -} - -// -// Get the margin for the end edge of the notification. -// -gint foobar_notification_widget_get_inset_end( FoobarNotificationWidget* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); - return self->inset_end; -} - -// -// Get the margin for the top edge of the notification. -// -gint foobar_notification_widget_get_inset_top( FoobarNotificationWidget* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); - return self->inset_top; -} - -// -// Get the margin for the bottom edge of the notification. -// -gint foobar_notification_widget_get_inset_bottom( FoobarNotificationWidget* self ) -{ - g_return_val_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ), 0 ); - return self->inset_bottom; -} - -// -// Update the notification model currently displayed. -// -void foobar_notification_widget_set_notification( - FoobarNotificationWidget* self, - FoobarNotification* value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); - - if ( self->notification != value ) - { - g_clear_object( &self->notification ); - if ( value ) { self->notification = g_object_ref( value ); } - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_NOTIFICATION] ); - } -} - -// -// Update the action invoked when the user wants to "close" a notification. -// -void foobar_notification_widget_set_close_action( - FoobarNotificationWidget* self, - FoobarNotificationCloseAction value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); - - if ( self->close_action != value ) - { - self->close_action = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CLOSE_ACTION] ); - } -} - -// -// Update the time format string used to display the notification's timestamp. -// -void foobar_notification_widget_set_time_format( - FoobarNotificationWidget* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); - - if ( g_strcmp0( self->time_format, value ) ) - { - g_clear_pointer( &self->time_format, g_free ); - self->time_format = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_TIME_FORMAT] ); - } -} - -// -// Update the minimum height of the notification (excluding the inset). -// -void foobar_notification_widget_set_min_height( - FoobarNotificationWidget* self, - gint value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); - - value = MAX( value, 0 ); - if ( self->min_height != value ) - { - self->min_height = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_MIN_HEIGHT] ); - gtk_widget_set_size_request( self->content, -1, self->min_height ); - } -} - -// -// Update the offset of the close button within the notification (may be negative, possibly increasing the actual inset). -// -void foobar_notification_widget_set_close_button_inset( - FoobarNotificationWidget* self, - gint value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); - - if ( self->close_button_inset != value ) - { - self->close_button_inset = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CLOSE_BUTTON_INSET] ); - foobar_notification_widget_update_margins( self ); - } -} - -// -// Update the margin for the start edge of the notification. -// -void foobar_notification_widget_set_inset_start( - FoobarNotificationWidget* self, - gint value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); - - value = MAX( value, 0 ); - if ( self->inset_start != value ) - { - self->inset_start = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_START] ); - foobar_notification_widget_update_margins( self ); - } -} - -// -// Update the margin for the end edge of the notification. -// -void foobar_notification_widget_set_inset_end( - FoobarNotificationWidget* self, - gint value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); - - value = MAX( value, 0 ); - if ( self->inset_end != value ) - { - self->inset_end = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_END] ); - foobar_notification_widget_update_margins( self ); - } -} - -// -// Update the margin for the top edge of the notification. -// -void foobar_notification_widget_set_inset_top( - FoobarNotificationWidget* self, - gint value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); - - value = MAX( value, 0 ); - if ( self->inset_top != value ) - { - self->inset_top = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_TOP] ); - foobar_notification_widget_update_margins( self ); - } -} - -// -// Update the margin for the bottom edge of the notification. -// -void foobar_notification_widget_set_inset_bottom( - FoobarNotificationWidget* self, - gint value ) -{ - g_return_if_fail( FOOBAR_IS_NOTIFICATION_WIDGET( self ) ); - - value = MAX( value, 0 ); - if ( self->inset_bottom != value ) - { - self->inset_bottom = value; - g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_BOTTOM] ); - foobar_notification_widget_update_margins( self ); - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Signal Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Called when the mouse cursor enters the notification. -// -// We will temporarily block the dismissal timeout for the notification and show the close button. -// -void foobar_notification_widget_handle_enter( - GtkEventControllerMotion* controller, - gdouble x, - gdouble y, - gpointer userdata ) -{ - (void)controller; - (void)x; - (void)y; - FoobarNotificationWidget* self = (FoobarNotificationWidget*)userdata; - - gtk_widget_set_visible( self->close_button, TRUE ); - if ( self->notification ) { foobar_notification_block_timeout( self->notification ); } -} - -// -// Called when the mouse cursor leaves the notification. -// -// We will unblock the dismissal timeout for the notification and hide the close button. -// -void foobar_notification_widget_handle_leave( - GtkEventControllerMotion* controller, - gpointer userdata ) -{ - (void)controller; - FoobarNotificationWidget* self = (FoobarNotificationWidget*)userdata; - - gtk_widget_set_visible( self->close_button, FALSE ); - if ( self->notification ) { foobar_notification_resume_timeout( self->notification ); } -} - -// -// Called when the user has clicked the "close" button. -// -// This either marks the notification as dismissed or removes it. -// -void foobar_notification_widget_handle_close_clicked( - GtkButton* button, - gpointer userdata ) -{ - (void)button; - FoobarNotificationWidget* self = (FoobarNotificationWidget*)userdata; - - if ( self->notification ) - { - switch ( self->close_action ) - { - case FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE: - foobar_notification_close( self->notification ); - break; - case FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS: - foobar_notification_dismiss( self->notification ); - break; - default: - g_warn_if_reached( ); - break; - } - } -} - -// --------------------------------------------------------------------------------------------------------------------- -// Value Converters -// --------------------------------------------------------------------------------------------------------------------- - -// -// Derive the timestamp label value from the notification's timestamp and the selected time format. -// -gchar* foobar_notification_widget_compute_time_label( - GtkExpression* expression, - GDateTime* time, - gchar const* format, - gpointer userdata ) -{ - (void)expression; - (void)userdata; - - return format != NULL ? g_date_time_format( time, format ) : NULL; -} - -// -// Derive the visibility of the notification icon from its value. -// -gboolean foobar_notification_widget_compute_icon_visible( - GtkExpression* expression, - GIcon* icon, - gpointer userdata ) -{ - (void)expression; - (void)userdata; - - return icon != NULL; -} - -// -// Derive the visibility of the notification body label from its value. -// -gboolean foobar_notification_widget_compute_body_visible( - GtkExpression* expression, - gchar const* body, - gpointer userdata ) -{ - (void)expression; - (void)userdata; - - return body != NULL && g_strcmp0( body, "" ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Methods -// --------------------------------------------------------------------------------------------------------------------- - -// -// Update the margins of the content widget and close button to the current inset values. -// -void foobar_notification_widget_update_margins( FoobarNotificationWidget* self ) -{ - gint inset = MAX( self->close_button_inset, 0 ); - gint overrun = MAX( -self->close_button_inset, 0 ); - gint actual_inset_end = MAX( self->inset_end, overrun ); - gint actual_inset_top = MAX( self->inset_top, overrun ); - - gtk_widget_set_margin_start( self->container, self->inset_start ); - gtk_widget_set_margin_end( self->container, actual_inset_end - overrun ); - gtk_widget_set_margin_top( self->container, actual_inset_top - overrun ); - gtk_widget_set_margin_bottom( self->container, self->inset_bottom ); - - gtk_widget_set_margin_end( self->content, overrun ); - gtk_widget_set_margin_top( self->content, overrun ); - - gtk_widget_set_margin_end( self->close_button, inset ); - gtk_widget_set_margin_top( self->close_button, inset ); -} diff --git a/src/widgets/notification-widget.h b/src/widgets/notification-widget.h deleted file mode 100644 index af9de49..0000000 --- a/src/widgets/notification-widget.h +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include -#include "services/notification-service.h" - -G_BEGIN_DECLS - -#define FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION foobar_notification_close_action_get_type( ) -#define FOOBAR_TYPE_NOTIFICATION_WIDGET foobar_notification_widget_get_type( ) - -typedef enum -{ - FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_REMOVE, - FOOBAR_TYPE_NOTIFICATION_CLOSE_ACTION_DISMISS, -} FoobarNotificationCloseAction; - -GType foobar_notification_close_action_get_type( void ); - -G_DECLARE_FINAL_TYPE( FoobarNotificationWidget, foobar_notification_widget, FOOBAR, NOTIFICATION_WIDGET, GtkWidget ) - -GtkWidget* foobar_notification_widget_new ( void ); -FoobarNotification* foobar_notification_widget_get_notification ( FoobarNotificationWidget* self ); -FoobarNotificationCloseAction foobar_notification_widget_get_close_action ( FoobarNotificationWidget* self ); -gchar const* foobar_notification_widget_get_time_format ( FoobarNotificationWidget* self ); -gint foobar_notification_widget_get_min_height ( FoobarNotificationWidget* self ); -gint foobar_notification_widget_get_close_button_inset( FoobarNotificationWidget* self ); -gint foobar_notification_widget_get_inset_start ( FoobarNotificationWidget* self ); -gint foobar_notification_widget_get_inset_end ( FoobarNotificationWidget* self ); -gint foobar_notification_widget_get_inset_top ( FoobarNotificationWidget* self ); -gint foobar_notification_widget_get_inset_bottom ( FoobarNotificationWidget* self ); -void foobar_notification_widget_set_notification ( FoobarNotificationWidget* self, - FoobarNotification* value ); -void foobar_notification_widget_set_close_action ( FoobarNotificationWidget* self, - FoobarNotificationCloseAction value ); -void foobar_notification_widget_set_time_format ( FoobarNotificationWidget* self, - gchar const* value ); -void foobar_notification_widget_set_min_height ( FoobarNotificationWidget* self, - gint value ); -void foobar_notification_widget_set_close_button_inset( FoobarNotificationWidget* self, - gint value ); -void foobar_notification_widget_set_inset_start ( FoobarNotificationWidget* self, - gint value ); -void foobar_notification_widget_set_inset_end ( FoobarNotificationWidget* self, - gint value ); -void foobar_notification_widget_set_inset_top ( FoobarNotificationWidget* self, - gint value ); -void foobar_notification_widget_set_inset_bottom ( FoobarNotificationWidget* self, - gint value ); - -G_END_DECLS diff --git a/src/widgets/panel/meson.build b/src/widgets/panel/meson.build deleted file mode 100644 index 10afc94..0000000 --- a/src/widgets/panel/meson.build +++ /dev/null @@ -1,7 +0,0 @@ -foobar_sources += files( - 'panel-item.c', - 'panel-item-icon.c', - 'panel-item-clock.c', - 'panel-item-workspaces.c', - 'panel-item-status.c', -) diff --git a/src/widgets/panel/panel-item-clock.c b/src/widgets/panel/panel-item-clock.c deleted file mode 100644 index 00b2cc5..0000000 --- a/src/widgets/panel/panel-item-clock.c +++ /dev/null @@ -1,152 +0,0 @@ -#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 ); -} diff --git a/src/widgets/panel/panel-item-clock.h b/src/widgets/panel/panel-item-clock.h deleted file mode 100644 index 5ff7e5b..0000000 --- a/src/widgets/panel/panel-item-clock.h +++ /dev/null @@ -1,15 +0,0 @@ -#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 diff --git a/src/widgets/panel/panel-item-icon.c b/src/widgets/panel/panel-item-icon.c deleted file mode 100644 index 0a3b4e9..0000000 --- a/src/widgets/panel/panel-item-icon.c +++ /dev/null @@ -1,92 +0,0 @@ -#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 ); -} diff --git a/src/widgets/panel/panel-item-icon.h b/src/widgets/panel/panel-item-icon.h deleted file mode 100644 index 2b4ac1f..0000000 --- a/src/widgets/panel/panel-item-icon.h +++ /dev/null @@ -1,13 +0,0 @@ -#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 diff --git a/src/widgets/panel/panel-item-status.c b/src/widgets/panel/panel-item-status.c deleted file mode 100644 index f06bb68..0000000 --- a/src/widgets/panel/panel-item-status.c +++ /dev/null @@ -1,955 +0,0 @@ -#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 ); - } -} diff --git a/src/widgets/panel/panel-item-status.h b/src/widgets/panel/panel-item-status.h deleted file mode 100644 index 0f1cfdb..0000000 --- a/src/widgets/panel/panel-item-status.h +++ /dev/null @@ -1,25 +0,0 @@ -#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 diff --git a/src/widgets/panel/panel-item-workspaces.c b/src/widgets/panel/panel-item-workspaces.c deleted file mode 100644 index dff629b..0000000 --- a/src/widgets/panel/panel-item-workspaces.c +++ /dev/null @@ -1,327 +0,0 @@ -#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 ); -} diff --git a/src/widgets/panel/panel-item-workspaces.h b/src/widgets/panel/panel-item-workspaces.h deleted file mode 100644 index d179217..0000000 --- a/src/widgets/panel/panel-item-workspaces.h +++ /dev/null @@ -1,16 +0,0 @@ -#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 diff --git a/src/widgets/panel/panel-item.c b/src/widgets/panel/panel-item.c deleted file mode 100644 index 918d9f5..0000000 --- a/src/widgets/panel/panel-item.c +++ /dev/null @@ -1,195 +0,0 @@ -#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; - } -} diff --git a/src/widgets/panel/panel-item.h b/src/widgets/panel/panel-item.h deleted file mode 100644 index 12a7e35..0000000 --- a/src/widgets/panel/panel-item.h +++ /dev/null @@ -1,23 +0,0 @@ -#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 diff --git a/subprojects/gtk4-layer-shell.wrap b/subprojects/gtk4-layer-shell.wrap index 276d77a..7f0e700 100644 --- a/subprojects/gtk4-layer-shell.wrap +++ b/subprojects/gtk4-layer-shell.wrap @@ -1,4 +1,4 @@ [wrap-git] url = https://github.com/wmww/gtk4-layer-shell.git -revision = v1.0.2 +revision = v1.1.1 depth = 1 -- cgit v1.3.1