diff --git a/README.md b/README.md index 9c262485..63749db9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ You'll need the following dependencies: libgranite-7-dev >= 7.7.0 libglib2.0-dev - libgtk-4-dev + libgtk-4-dev >= 4.22 libnotify-dev libpulse-dev libwingpanel-9-dev diff --git a/data/24/volume.svg b/data/24/volume.svg new file mode 100644 index 00000000..6d3135c0 --- /dev/null +++ b/data/24/volume.svg @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/gresource.xml b/data/gresource.xml index c90ac866..ea7d6dcb 100644 --- a/data/gresource.xml +++ b/data/gresource.xml @@ -2,6 +2,7 @@ indicator.css + 24/volume.svg 16/audio-subwoofer-symbolic.svg diff --git a/meson.build b/meson.build index f11e0bd2..90ed7a2c 100644 --- a/meson.build +++ b/meson.build @@ -52,6 +52,7 @@ shared_module( 'src/Widgets/PlayerList.vala', 'src/Widgets/DeviceManagerWidget.vala', 'src/Widgets/DeviceItem.vala', + 'src/Widgets/Symbol.vala', 'src/Services/PulseAudioManager.vala', 'src/Services/MprisClient.vala', 'src/Services/Volume-control.vala', @@ -65,7 +66,7 @@ shared_module( dependency('gio-2.0'), dependency('gobject-2.0'), dependency('granite-7', version: '>=7.7.0'), - dependency('gtk4'), + dependency('gtk4', version: '>=4.22'), dependency('gtk4-wayland'), dependency('libpulse'), dependency('libpulse-mainloop-glib'), diff --git a/src/Indicator.vala b/src/Indicator.vala index 86787edb..e75bbe2a 100644 --- a/src/Indicator.vala +++ b/src/Indicator.vala @@ -115,7 +115,7 @@ public class Sound.Indicator : Wingpanel.Indicator { display_widget.volume_press_event.connect (volume_control.toggle_mute); display_widget.mic_press_event.connect (volume_control.toggle_mic_mute); - display_widget.icon_name = get_volume_icon (volume_control.volume.volume); + display_widget.volume_state = get_volume_state (volume_control.volume.volume); display_widget.volume_scroll_event.connect_after (on_volume_icon_scroll_event); display_widget.mic_scroll_event.connect_after (on_mic_icon_scroll_event); @@ -268,7 +268,7 @@ public class Sound.Indicator : Wingpanel.Indicator { double volume = volume_control.volume.volume / max_volume; if (volume != volume_adjustment.get_value ()) { volume_adjustment.set_value (volume); - display_widget.icon_name = get_volume_icon (volume); + display_widget.volume_state = get_volume_state (volume); } } @@ -283,13 +283,12 @@ public class Sound.Indicator : Wingpanel.Indicator { private void on_mute_change () { volume_scale.active = !volume_control.mute; - string volume_icon = get_volume_icon (volume_control.volume.volume); - display_widget.icon_name = volume_icon; + display_widget.volume_state = get_volume_state (volume_control.volume.volume); if (volume_control.mute) { volume_scale.icon = "audio-volume-muted-symbolic"; } else { - volume_scale.icon = volume_icon; + volume_scale.icon = get_volume_icon (volume_control.volume.volume); } } @@ -320,12 +319,12 @@ public class Sound.Indicator : Wingpanel.Indicator { sound_was_blocked_timeout_id = Timeout.add_seconds (5, () => { mute_blocks_sound = false; sound_was_blocked_timeout_id = 0; - display_widget.icon_name = get_volume_icon (volume_control.volume.volume); + display_widget.volume_state = get_volume_state (volume_control.volume.volume); return false; }); } - display_widget.icon_name = get_volume_icon (volume_control.volume.volume); + display_widget.volume_state = get_volume_state (volume_control.volume.volume); } private void on_volume_icon_scroll_event (Value event_value) { @@ -356,6 +355,18 @@ public class Sound.Indicator : Wingpanel.Indicator { } } + private unowned string get_volume_state (double volume) { + if (volume <= 0 || this.volume_control.mute) { + return this.mute_blocks_sound ? "disabled-blocking" : SoundIndicator.SymbolState.DISABLED; + } else if (volume <= 0.3) { + return "low"; + } else if (volume <= 0.7) { + return SoundIndicator.SymbolState.NORMAL; + } else { + return "high"; + } + } + private void on_volume_switch_change () { if (volume_scale.active) { volume_control.set_mute (false); diff --git a/src/Widgets/DisplayWidget.vala b/src/Widgets/DisplayWidget.vala index 56c13a46..10fe18f2 100644 --- a/src/Widgets/DisplayWidget.vala +++ b/src/Widgets/DisplayWidget.vala @@ -21,7 +21,7 @@ public class Sound.DisplayWidget : Gtk.Box { public bool show_mic { get; set; } public bool mic_muted { get; set; } - public string icon_name { get; set; } + public string volume_state { get; set; } // HACK: Using Gdk.ScrollEvent instead of Value as the type of the parameter // resulsts build error with valac 0.56.18 @@ -30,7 +30,7 @@ public class Sound.DisplayWidget : Gtk.Box { public signal void mic_scroll_event (Value event_value); construct { - var volume_icon = new Gtk.Image () { + var volume_symbol = new SoundIndicator.Symbol ("/io/elementary/wingpanel/sound/24/volume.svg") { pixel_size = 24 }; @@ -45,7 +45,7 @@ public class Sound.DisplayWidget : Gtk.Box { valign = Gtk.Align.CENTER; append (mic_revealer); - append (volume_icon); + append (volume_symbol); var mic_scroll_controller = new Gtk.EventControllerLegacy (); mic_scroll_controller.event.connect ((e) => { @@ -72,7 +72,7 @@ public class Sound.DisplayWidget : Gtk.Box { }); mic_icon.add_controller (mic_scroll_controller); - volume_icon.add_controller (volume_scroll_controller); + volume_symbol.add_controller (volume_scroll_controller); var mic_gesture_click = new Gtk.GestureClick () { button = Gdk.BUTTON_MIDDLE @@ -94,12 +94,12 @@ public class Sound.DisplayWidget : Gtk.Box { volume_gesture_click.reset (); }); - volume_icon.add_controller (volume_gesture_click); + volume_symbol.add_controller (volume_gesture_click); bind_property ( - "icon-name", - volume_icon, - "icon-name", + "volume-state", + volume_symbol, + "state", GLib.BindingFlags.BIDIRECTIONAL | GLib.BindingFlags.SYNC_CREATE ); bind_property ( diff --git a/src/Widgets/Symbol.vala b/src/Widgets/Symbol.vala new file mode 100644 index 00000000..e131bc57 --- /dev/null +++ b/src/Widgets/Symbol.vala @@ -0,0 +1,78 @@ +/* +* SPDX-License-Identifier: LGPL-2.1-or-later +* SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io) +*/ + +namespace SoundIndicator.SymbolState { + // The default state + public const string NORMAL = "normal"; + // Disabled state represented by a slash + public const string DISABLED = "disabled"; + // e.g. paired, connected, needs attention + public const string ACTIVE = "active"; +} + +public class SoundIndicator.Symbol : Granite.Bin { + public string resource_path { get; construct; } + + public int pixel_size { + get { return image.pixel_size; } + set { image.pixel_size = value; } + } + + public uint state_index { + get { return svg.state; } + set { + uint length = -1; + svg.get_state_names (out length); + + if (value > length - 1) { + warning ("Granite.Symbol set to undefined state. Ignoring."); + return; + } + + svg.state = value; + notify_property ("state"); + } + } + + public string state { + get { + uint length = -1; + return svg.get_state_names (out length)[state_index]; + } + set { + uint length = -1; + var names = svg.get_state_names (out length); + + for (int i = 0; i < length; i++) { + if (names[i] == value) { + state_index = i; + return; + } + } + + warning ("Granite.Symbol set to undefined state. Ignoring."); + } + } + + private Gtk.Image image; + private Gtk.Svg svg; + + public Symbol (string resource_path) { + Object (resource_path: resource_path); + } + + construct { + svg = new Gtk.Svg.from_resource (resource_path); + + image = new Gtk.Image.from_paintable (svg); + + child = image; + + child.realize.connect (() => { + svg.set_frame_clock (get_frame_clock ()); + svg.play (); + }); + } +}