roundabout,
created on Monday, 18 August 2025, 19:37:14 (1755545834),
received on Monday, 18 August 2025, 19:37:17 (1755545837)
Author identity: Vlad <vlad.muntoiu@gmail.com>
b44c01a26f9b57b18772d0fa45240c04c1a52e22
applets/volume/__init__.py
@@ -21,6 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import localeimport panorama_panel
@@ -39,6 +40,11 @@ class Volume(panorama_panel.Applet):
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None): super().__init__() if config is None: config = {} self.popdown_after_manual = config.get("popdown_after_manual", 2000) self.percentage_reveal = config.get("percentage_reveal", 1000) self.percentage_animation_time = config.get("percentage_animation_time", 250)asyncio.create_task(self.main()) async def main(self):
@@ -57,9 +63,12 @@ class Volume(panorama_panel.Applet):
self.volume_scale_changed_handler = self.volume_scale.connect("value-changed", self.on_scale_changed) self.volume_popover = Gtk.Popover() self.volume_popover.set_child(self.volume_scale) self.volume_popover.connect("show", self.on_popover_show)self.menu_button.set_popover(self.volume_popover) self.menu_button.set_has_frame(False) self.button_content = Gtk.Box(orientation=self.get_orientation()) self.icon = Gtk.Image() self.button_content.append(self.icon) self.menu_button.set_child(self.button_content)scroll_controller = Gtk.EventControllerScroll.new(Gtk.EventControllerScrollFlags.VERTICAL) scroll_controller.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) self.volume_popover.add_controller(scroll_controller)
@@ -69,6 +78,7 @@ class Volume(panorama_panel.Applet):
self.add_controller(scroll_controller) scroll_controller.connect("scroll", self.on_button_scroll) self.popdown_timeout = None self.conceal_timeout = Noneself.set_button_icon() gesture_click = Gtk.GestureClick() gesture_click.set_button(Gdk.BUTTON_MIDDLE)
@@ -76,9 +86,47 @@ class Volume(panorama_panel.Applet):
self.add_controller(gesture_click) self.append(self.menu_button) self.revealer = Gtk.Revealer(transition_duration=self.percentage_animation_time) self.button_content.append(self.revealer) if self.get_orientation() == Gtk.Orientation.HORIZONTAL: self.revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_RIGHT) elif self.get_orientation() == Gtk.Orientation.VERTICAL: self.revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_DOWN) self.percentage_label = Gtk.Label() self.percentage_label.set_width_chars(5) self.update_text() self.revealer.set_child(self.percentage_label) self.volume_popover.connect("show", self.popover_shown) self.volume_popover.connect("closed", self.popover_hidden) if not self.percentage_reveal: self.revealer.set_reveal_child(True) loop = asyncio.get_event_loop() loop.create_task(self.listen()) def popover_shown(self, *args): if self.percentage_reveal != -1: self.revealer.set_reveal_child(True) def popover_hidden(self, *args): if self.percentage_reveal: self.revealer.set_reveal_child(False) def set_panel_position(self, position: Gtk.PositionType): if not hasattr(self, "revealer"): return if position in {Gtk.PositionType.TOP, Gtk.PositionType.BOTTOM}: self.revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_RIGHT) self.button_content.set_orientation(Gtk.Orientation.HORIZONTAL) elif position in {Gtk.PositionType.LEFT, Gtk.PositionType.RIGHT}: self.revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_DOWN) self.button_content.set_orientation(Gtk.Orientation.VERTICAL) def on_middle_click_released(self, gesture, n_press, x, y): loop = asyncio.get_event_loop() loop.create_task(self.idle_on_middle_click_released())
@@ -92,22 +140,17 @@ class Volume(panorama_panel.Applet):
self.default_sink.mute = True self.set_button_icon() def on_popover_show(self, popover):if self.popdown_timeout is not None:GLib.source_remove(self.popdown_timeout)self.popdown_timeout = GLib.timeout_add(2000, self.close_scale_popover)def set_button_icon(self): volume = self.current_volume threshold = 1.0 / 3.0threshold = 1 / 3if volume == 0.0 or self.default_sink.mute: self.menu_button.set_icon_name("audio-volume-muted-symbolic")self.icon.set_from_icon_name("audio-volume-muted-symbolic")elif volume < threshold: self.menu_button.set_icon_name("audio-volume-low-symbolic")self.icon.set_from_icon_name("audio-volume-low-symbolic")elif volume < threshold * 2: self.menu_button.set_icon_name("audio-volume-medium-symbolic")self.icon.set_from_icon_name("audio-volume-medium-symbolic")else: self.menu_button.set_icon_name("audio-volume-high-symbolic")self.icon.set_from_icon_name("audio-volume-high-symbolic")def on_button_scroll(self, controller, dx, dy): if dy < 0 or dy > 0:
@@ -128,9 +171,11 @@ class Volume(panorama_panel.Applet):
self.volume_scale.set_value(new_volume) self.current_volume = new_volume self.set_button_icon() if self.popdown_timeout is not None: GLib.source_remove(self.popdown_timeout) self.popdown_timeout = GLib.timeout_add(2000, self.close_scale_popover)if self.popdown_after_manual: self.popdown_timeout = GLib.timeout_add(self.popdown_after_manual, self.close_scale_popover)return Gdk.EVENT_STOP async def idle_volume_update(self):
@@ -138,15 +183,20 @@ class Volume(panorama_panel.Applet):
await self.pulse.volume_set_all_chans(self.default_sink, new_volume) self.current_volume = new_volume self.set_button_icon() if self.popdown_timeout is not None: GLib.source_remove(self.popdown_timeout) self.popdown_timeout = GLib.timeout_add(2000, self.close_scale_popover)if self.popdown_after_manual: self.popdown_timeout = GLib.timeout_add(self.popdown_after_manual, self.close_scale_popover)return False def on_scale_changed(self, scale, *args): loop = asyncio.get_event_loop() loop.create_task(self.idle_volume_update()) def update_text(self): self.percentage_label.set_text(f"{locale.format_string('%d', self.current_volume * 100)}%") async def idle_update_scale(self): server_info = await self.pulse.server_info() self.default_sink = await self.pulse.get_sink_by_name(server_info.default_sink_name)
@@ -157,11 +207,25 @@ class Volume(panorama_panel.Applet):
self.current_volume = new_volume self.volume_scale.set_value(self.current_volume) self.set_button_icon() if self.popdown_timeout is not None:GLib.source_remove(self.popdown_timeout)self.popdown_timeout = GLib.timeout_add(2000, self.close_scale_popover)self.update_text() if self.percentage_reveal != -1: self.revealer.set_reveal_child(True) if self.conceal_timeout is not None: GLib.source_remove(self.conceal_timeout) if self.percentage_reveal > 0: self.conceal_timeout = GLib.timeout_add(self.percentage_reveal, self.conceal_percentage)return False def conceal_percentage(self): if not self.percentage_reveal: return if not self.volume_popover.get_visible(): self.revealer.set_reveal_child(False) self.conceal_timeout = None def close_scale_popover(self): self.popdown_timeout = None self.menu_button.popdown()
@@ -177,3 +241,10 @@ class Volume(panorama_panel.Applet):
loop.create_task(self.idle_update_scale()) except pulsectl_asyncio.PulseOperationFailed: print(f"Could not retrieve info for sink index {event.index}") def get_config(self): return { "percentage_reveal": self.percentage_reveal, "popdown_after_manual": self.popdown_after_manual, "percentage_animation_time": self.percentage_animation_time, }
config.yaml
@@ -50,7 +50,10 @@ panels:
centre: - NotifierApplet: {} right: - Volume: {}- Volume: percentage_reveal: 1000 popdown_after_manual: 2000 percentage_animation_time: 250- ClockApplet: formatting: '%T, %a %-d %b %Y' - position: bottom