from fabric.audio.service import Audio from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.circularprogressbar import CircularProgressBar from fabric.widgets.eventbox import EventBox from fabric.widgets.label import Label from fabric.widgets.overlay import Overlay from fabric.widgets.scale import Scale from gi.repository import Gdk, GLib import config.data as data import modules.icons as icons from services.brightness import Brightness class VolumeSlider(Scale): def __init__(self, **kwargs): super().__init__( name="control-slider", orientation="h", h_expand=True, h_align="fill", has_origin=True, increments=(0.01, 0.1), **kwargs, ) self.audio = Audio() self.audio.connect("notify::speaker", self.on_new_speaker) if self.audio.speaker: self.audio.speaker.connect("changed", self.on_speaker_changed) self.connect("change-value", self.on_change_value) self.add_style_class("vol") self._pending_value = None self._update_source_id = None self._debounce_timeout = 100 self._updating_from_audio = False self.on_speaker_changed() def on_new_speaker(self, *args): if self.audio.speaker: self.audio.speaker.connect("changed", self.on_speaker_changed) self.on_speaker_changed() def on_change_value(self, widget, scroll, value): if self._updating_from_audio: return False if self.audio.speaker: self._pending_value = value * 100 if self._update_source_id is not None: GLib.source_remove(self._update_source_id) self._update_source_id = GLib.timeout_add( self._debounce_timeout, self._update_volume_callback ) return False def _update_volume_callback(self): if self._pending_value is not None and self.audio.speaker: self.audio.speaker.volume = self._pending_value self._pending_value = None self._update_source_id = None return False def on_speaker_changed(self, *_): if not self.audio.speaker: return self._updating_from_audio = True self.value = self.audio.speaker.volume / 100 self._updating_from_audio = False if self.audio.speaker.muted: self.add_style_class("muted") else: self.remove_style_class("muted") class MicSlider(Scale): def __init__(self, **kwargs): super().__init__( name="control-slider", orientation="h", h_expand=True, has_origin=True, increments=(0.01, 0.1), **kwargs, ) self.audio = Audio() self.audio.connect("notify::microphone", self.on_new_microphone) if self.audio.microphone: self.audio.microphone.connect("changed", self.on_microphone_changed) self.connect("change-value", self.on_change_value) self.add_style_class("mic") self._updating_from_audio = False self.on_microphone_changed() def on_new_microphone(self, *args): if self.audio.microphone: self.audio.microphone.connect("changed", self.on_microphone_changed) self.on_microphone_changed() def on_change_value(self, widget, scroll, value): if self._updating_from_audio: return False if self.audio.microphone: self.audio.microphone.volume = value * 100 return False def on_microphone_changed(self, *_): if not self.audio.microphone: return self._updating_from_audio = True self.value = self.audio.microphone.volume / 100 self._updating_from_audio = False if self.audio.microphone.muted: self.add_style_class("muted") else: self.remove_style_class("muted") class BrightnessSlider(Scale): def __init__(self, **kwargs): super().__init__( name="control-slider", orientation="h", h_expand=True, has_origin=True, increments=(5, 10), **kwargs, ) self.client = Brightness.get_initial() if self.client.screen_brightness == -1: self.destroy() return self.set_range(0, self.client.max_screen) self.set_value(self.client.screen_brightness) self.add_style_class("brightness") self._pending_value = None self._update_source_id = None self._updating_from_brightness = False self._debounce_timeout = 100 self.connect("change-value", self.on_scale_move) self.client.connect("screen", self.on_brightness_changed) def on_scale_move(self, widget, scroll, moved_pos): if self._updating_from_brightness: return False self._pending_value = moved_pos if self._update_source_id is not None: GLib.source_remove(self._update_source_id) self._update_source_id = GLib.timeout_add( self._debounce_timeout, self._update_brightness_callback ) return False def _update_brightness_callback(self): if self._pending_value is not None: value_to_set = self._pending_value self._pending_value = None if value_to_set != self.client.screen_brightness: self.client.screen_brightness = value_to_set self._update_source_id = None return False else: self._update_source_id = None return False def on_brightness_changed(self, client, _): self._updating_from_brightness = True self.set_value(self.client.screen_brightness) self._updating_from_brightness = False percentage = int((self.client.screen_brightness / self.client.max_screen) * 100) self.set_tooltip_text(f"{percentage}%") def destroy(self): if self._update_source_id is not None: GLib.source_remove(self._update_source_id) super().destroy() class BrightnessSmall(Box): def __init__(self, **kwargs): super().__init__(name="button-bar-brightness", **kwargs) self.brightness = Brightness.get_initial() if self.brightness.screen_brightness == -1: self.destroy() return self.progress_bar = CircularProgressBar( name="button-brightness", size=28, line_width=2, start_angle=150, end_angle=390, ) self.brightness_label = Label( name="brightness-label", markup=icons.brightness_high ) self.brightness_button = Button(child=self.brightness_label) self.event_box = EventBox( events=["scroll", "smooth-scroll"], child=Overlay(child=self.progress_bar, overlays=self.brightness_button), ) self.event_box.connect("scroll-event", self.on_scroll) self.add(self.event_box) self.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK) self._updating_from_brightness = False self._pending_value = None self._update_source_id = None self._debounce_timeout = 100 # Don't connect to progress_bar value changes - only use scroll events self.brightness.connect("screen", self.on_brightness_changed) self.on_brightness_changed() def on_scroll(self, widget, event): if self.brightness.max_screen == -1: return step_size = 5 current_brightness = self.brightness.screen_brightness if event.delta_y < 0: new_brightness = min( current_brightness + step_size, self.brightness.max_screen ) elif event.delta_y > 0: new_brightness = max(current_brightness - step_size, 0) else: return # Directly update brightness, don't touch progress_bar self._pending_value = new_brightness if self._update_source_id is not None: GLib.source_remove(self._update_source_id) self._update_source_id = GLib.timeout_add( self._debounce_timeout, self._update_brightness_callback ) def _update_brightness_callback(self): if ( self._pending_value is not None and self._pending_value != self.brightness.screen_brightness ): self.brightness.screen_brightness = self._pending_value self._pending_value = None self._update_source_id = None return False def on_brightness_changed(self, *args): if self.brightness.max_screen == -1: return normalized = ( self.brightness.screen_brightness / self.brightness.max_screen if self.brightness.max_screen > 0 else 0 ) self._updating_from_brightness = True self.progress_bar.value = normalized self._updating_from_brightness = False brightness_percentage = int(normalized * 100) if brightness_percentage >= 75: self.brightness_label.set_markup(icons.brightness_high) elif brightness_percentage >= 24: self.brightness_label.set_markup(icons.brightness_medium) else: self.brightness_label.set_markup(icons.brightness_low) self.set_tooltip_text(f"{brightness_percentage}%") def destroy(self): if self._update_source_id is not None: GLib.source_remove(self._update_source_id) super().destroy() class VolumeSmall(Box): def __init__(self, **kwargs): super().__init__(name="button-bar-vol", **kwargs) self.audio = Audio() self.progress_bar = CircularProgressBar( name="button-volume", size=28, line_width=2, start_angle=150, end_angle=390, ) self.vol_label = Label(name="vol-label", markup=icons.vol_high) self.vol_button = Button(on_clicked=self.toggle_mute, child=self.vol_label) self.event_box = EventBox( events=["scroll", "smooth-scroll"], child=Overlay(child=self.progress_bar, overlays=self.vol_button), ) self.audio.connect("notify::speaker", self.on_new_speaker) if self.audio.speaker: self.audio.speaker.connect("changed", self.on_speaker_changed) self.event_box.connect("scroll-event", self.on_scroll) self.add(self.event_box) self.on_speaker_changed() self.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK) def on_new_speaker(self, *args): if self.audio.speaker: self.audio.speaker.connect("changed", self.on_speaker_changed) self.on_speaker_changed() def toggle_mute(self, event): current_stream = self.audio.speaker if current_stream: current_stream.muted = not current_stream.muted if current_stream.muted: self.on_speaker_changed() self.progress_bar.add_style_class("muted") self.vol_label.add_style_class("muted") else: self.on_speaker_changed() self.progress_bar.remove_style_class("muted") self.vol_label.remove_style_class("muted") def on_scroll(self, _, event): if not self.audio.speaker: return if event.direction == Gdk.ScrollDirection.SMOOTH: if abs(event.delta_y) > 0: self.audio.speaker.volume -= event.delta_y if abs(event.delta_x) > 0: self.audio.speaker.volume += event.delta_x def on_speaker_changed(self, *_): if not self.audio.speaker: return vol_high_icon = icons.vol_high vol_medium_icon = icons.vol_medium vol_mute_icon = icons.vol_off vol_off_icon = icons.vol_mute if "bluetooth" in self.audio.speaker.icon_name: vol_high_icon = icons.bluetooth_connected vol_medium_icon = icons.bluetooth vol_mute_icon = icons.bluetooth_off vol_off_icon = icons.bluetooth_disconnected self.progress_bar.value = self.audio.speaker.volume / 100 if self.audio.speaker.muted: self.vol_button.get_child().set_markup(vol_mute_icon) self.progress_bar.add_style_class("muted") self.vol_label.add_style_class("muted") self.set_tooltip_text("Muted") return else: self.progress_bar.remove_style_class("muted") self.vol_label.remove_style_class("muted") self.set_tooltip_text(f"{round(self.audio.speaker.volume)}%") if self.audio.speaker.volume > 74: self.vol_button.get_child().set_markup(vol_high_icon) elif self.audio.speaker.volume > 0: self.vol_button.get_child().set_markup(vol_medium_icon) else: self.vol_button.get_child().set_markup(vol_off_icon) class MicSmall(Box): def __init__(self, **kwargs): super().__init__(name="button-bar-mic", **kwargs) self.audio = Audio() self.progress_bar = CircularProgressBar( name="button-mic", size=28, line_width=2, start_angle=150, end_angle=390, ) self.mic_label = Label(name="mic-label", markup=icons.mic) self.mic_button = Button(on_clicked=self.toggle_mute, child=self.mic_label) self.event_box = EventBox( events=["scroll", "smooth-scroll"], child=Overlay(child=self.progress_bar, overlays=self.mic_button), ) self.audio.connect("notify::microphone", self.on_new_microphone) if self.audio.microphone: self.audio.microphone.connect("changed", self.on_microphone_changed) self.event_box.connect("scroll-event", self.on_scroll) self.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK) self.add(self.event_box) self.on_microphone_changed() def on_new_microphone(self, *args): if self.audio.microphone: self.audio.microphone.connect("changed", self.on_microphone_changed) self.on_microphone_changed() def toggle_mute(self, event): current_stream = self.audio.microphone if current_stream: current_stream.muted = not current_stream.muted if current_stream.muted: self.mic_button.get_child().set_markup(icons.mic_mute) self.progress_bar.add_style_class("muted") self.mic_label.add_style_class("muted") else: self.on_microphone_changed() self.progress_bar.remove_style_class("muted") self.mic_label.remove_style_class("muted") def on_scroll(self, _, event): if not self.audio.microphone: return if event.direction == Gdk.ScrollDirection.SMOOTH: if abs(event.delta_y) > 0: self.audio.microphone.volume -= event.delta_y if abs(event.delta_x) > 0: self.audio.microphone.volume += event.delta_x def on_microphone_changed(self, *_): if not self.audio.microphone: return if self.audio.microphone.muted: self.mic_button.get_child().set_markup(icons.mic_mute) self.progress_bar.add_style_class("muted") self.mic_label.add_style_class("muted") self.set_tooltip_text("Muted") return else: self.progress_bar.remove_style_class("muted") self.mic_label.remove_style_class("muted") self.progress_bar.value = self.audio.microphone.volume / 100 self.set_tooltip_text(f"{round(self.audio.microphone.volume)}%") if self.audio.microphone.volume >= 1: self.mic_button.get_child().set_markup(icons.mic) else: self.mic_button.get_child().set_markup(icons.mic_mute) class BrightnessIcon(Box): def __init__(self, **kwargs): super().__init__(name="brightness-icon", **kwargs) self.brightness = Brightness.get_initial() if self.brightness.screen_brightness == -1: self.destroy() return self.brightness_label = Label( name="brightness-label-dash", markup=icons.brightness_high, h_align="center", v_align="center", h_expand=True, v_expand=True, ) self.brightness_button = Button( child=self.brightness_label, h_align="center", v_align="center", h_expand=True, v_expand=True, ) self.event_box = EventBox( events=["scroll", "smooth-scroll"], child=self.brightness_button, h_align="center", v_align="center", h_expand=True, v_expand=True, ) self.event_box.connect("scroll-event", self.on_scroll) self.add(self.event_box) self._pending_value = None self._update_source_id = None self._updating_from_brightness = False self.brightness.connect("screen", self.on_brightness_changed) self.on_brightness_changed() self.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK) def on_scroll(self, _, event): if self.brightness.max_screen == -1: return step_size = 5 current_brightness = self.brightness.screen_brightness if event.direction == Gdk.ScrollDirection.SMOOTH: if event.delta_y < 0: new_brightness = min( current_brightness + step_size, self.brightness.max_screen ) elif event.delta_y > 0: new_brightness = max(current_brightness - step_size, 0) else: return else: if event.direction == Gdk.ScrollDirection.UP: new_brightness = min( current_brightness + step_size, self.brightness.max_screen ) elif event.direction == Gdk.ScrollDirection.DOWN: new_brightness = max(current_brightness - step_size, 0) else: return self._pending_value = new_brightness if self._update_source_id is None: self._update_source_id = GLib.timeout_add( 100, self._update_brightness_callback ) def _update_brightness_callback(self): if ( self._pending_value is not None and self._pending_value != self.brightness.screen_brightness ): self.brightness.screen_brightness = self._pending_value self._pending_value = None return True else: self._update_source_id = None return False def on_brightness_changed(self, *args): if self.brightness.max_screen == -1: return self._updating_from_brightness = True normalized = self.brightness.screen_brightness / self.brightness.max_screen brightness_percentage = int(normalized * 100) if brightness_percentage >= 75: self.brightness_label.set_markup("󰃠") elif brightness_percentage >= 24: self.brightness_label.set_markup("󰃠") else: self.brightness_label.set_markup("󰃠") self.set_tooltip_text(f"{brightness_percentage}%") self._updating_from_brightness = False def destroy(self): if self._update_source_id is not None: GLib.source_remove(self._update_source_id) super().destroy() class VolumeIcon(Box): def __init__(self, **kwargs): super().__init__(name="vol-icon", **kwargs) self.audio = Audio() self.vol_label = Label( name="vol-label-dash", markup="", h_align="center", v_align="center", h_expand=True, v_expand=True, ) self.vol_button = Button( on_clicked=self.toggle_mute, child=self.vol_label, h_align="center", v_align="center", h_expand=True, v_expand=True, ) self.event_box = EventBox( events=["scroll", "smooth-scroll"], child=self.vol_button, h_align="center", v_align="center", h_expand=True, v_expand=True, ) self.event_box.connect("scroll-event", self.on_scroll) self.add(self.event_box) self._pending_value = None self._update_source_id = None self._periodic_update_source_id = None self.audio.connect("notify::speaker", self.on_new_speaker) if self.audio.speaker: self.audio.speaker.connect("changed", self.on_speaker_changed) self._periodic_update_source_id = GLib.timeout_add_seconds( 2, self.update_device_icon ) self.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK) def on_scroll(self, _, event): if not self.audio.speaker: return step_size = 5 current_volume = self.audio.speaker.volume if event.direction == Gdk.ScrollDirection.SMOOTH: if event.delta_y < 0: new_volume = min(current_volume + step_size, 100) elif event.delta_y > 0: new_volume = max(current_volume - step_size, 0) else: return else: if event.direction == Gdk.ScrollDirection.UP: new_volume = min(current_volume + step_size, 100) elif event.direction == Gdk.ScrollDirection.DOWN: new_volume = max(current_volume - step_size, 0) else: return self._pending_value = new_volume if self._update_source_id is None: self._update_source_id = GLib.timeout_add(100, self._update_volume_callback) def _update_volume_callback(self): if ( self._pending_value is not None and self._pending_value != self.audio.speaker.volume ): self.audio.speaker.volume = self._pending_value self._pending_value = None return True else: self._update_source_id = None return False def on_new_speaker(self, *args): if self.audio.speaker: self.audio.speaker.connect("changed", self.on_speaker_changed) self.on_speaker_changed() def toggle_mute(self, event): current_stream = self.audio.speaker if current_stream: current_stream.muted = not current_stream.muted self.on_speaker_changed() def on_speaker_changed(self, *_): if not self.audio.speaker: self.vol_label.set_markup("") self.remove_style_class("muted") self.vol_label.remove_style_class("muted") self.vol_button.remove_style_class("muted") self.set_tooltip_text("No audio device") return if self.audio.speaker.muted: self.vol_label.set_markup(icons.headphones) self.add_style_class("muted") self.vol_label.add_style_class("muted") self.vol_button.add_style_class("muted") self.set_tooltip_text("Muted") else: self.remove_style_class("muted") self.vol_label.remove_style_class("muted") self.vol_button.remove_style_class("muted") self.update_device_icon() self.set_tooltip_text(f"{round(self.audio.speaker.volume)}%") def update_device_icon(self): if not self.audio.speaker: self.vol_label.set_markup("") return True if self.audio.speaker.muted: return True try: device_type = self.audio.speaker.port.type if device_type == "headphones": self.vol_label.set_markup(icons.headphones) elif device_type == "speaker": self.vol_label.set_markup(icons.headphones) else: self.vol_label.set_markup(icons.headphones) except AttributeError: self.vol_label.set_markup(icons.headphones) return True def destroy(self): if self._update_source_id is not None: GLib.source_remove(self._update_source_id) if ( hasattr(self, "_periodic_update_source_id") and self._periodic_update_source_id is not None ): GLib.source_remove(self._periodic_update_source_id) super().destroy() class MicIcon(Box): def __init__(self, **kwargs): super().__init__(name="mic-icon", **kwargs) self.audio = Audio() self.mic_label = Label( name="mic-label-dash", markup=icons.mic, h_align="center", v_align="center", h_expand=True, v_expand=True, ) self.mic_button = Button( on_clicked=self.toggle_mute, child=self.mic_label, h_align="center", v_align="center", h_expand=True, v_expand=True, ) self.event_box = EventBox( events=["scroll", "smooth-scroll"], child=self.mic_button, h_align="center", v_align="center", h_expand=True, v_expand=True, ) self.event_box.connect("scroll-event", self.on_scroll) self.add(self.event_box) self._pending_value = None self._update_source_id = None self.audio.connect("notify::microphone", self.on_new_microphone) if self.audio.microphone: self.audio.microphone.connect("changed", self.on_microphone_changed) self.on_microphone_changed() self.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK) def on_scroll(self, _, event): if not self.audio.microphone: return step_size = 5 current_volume = self.audio.microphone.volume if event.direction == Gdk.ScrollDirection.SMOOTH: if event.delta_y < 0: new_volume = min(current_volume + step_size, 100) elif event.delta_y > 0: new_volume = max(current_volume - step_size, 0) else: return else: if event.direction == Gdk.ScrollDirection.UP: new_volume = min(current_volume + step_size, 100) elif event.direction == Gdk.ScrollDirection.DOWN: new_volume = max(current_volume - step_size, 0) else: return self._pending_value = new_volume if self._update_source_id is None: self._update_source_id = GLib.timeout_add(100, self._update_volume_callback) def _update_volume_callback(self): if ( self._pending_value is not None and self._pending_value != self.audio.microphone.volume ): self.audio.microphone.volume = self._pending_value self._pending_value = None return True else: self._update_source_id = None return False def on_new_microphone(self, *args): if self.audio.microphone: self.audio.microphone.connect("changed", self.on_microphone_changed) self.on_microphone_changed() def toggle_mute(self, event): current_stream = self.audio.microphone if current_stream: current_stream.muted = not current_stream.muted if current_stream.muted: self.mic_button.get_child().set_markup("") self.mic_label.add_style_class("muted") self.mic_button.add_style_class("muted") else: self.on_microphone_changed() self.mic_label.remove_style_class("muted") self.mic_button.remove_style_class("muted") def on_microphone_changed(self, *_): if not self.audio.microphone: return if self.audio.microphone.muted: self.mic_button.get_child().set_markup("") self.add_style_class("muted") self.mic_label.add_style_class("muted") self.set_tooltip_text("Muted") return else: self.remove_style_class("muted") self.mic_label.remove_style_class("muted") self.set_tooltip_text(f"{round(self.audio.microphone.volume)}%") if self.audio.microphone.volume >= 1: self.mic_button.get_child().set_markup("") else: self.mic_button.get_child().set_markup("") def destroy(self): if self._update_source_id is not None: GLib.source_remove(self._update_source_id) super().destroy() class ControlSliders(Box): def __init__(self, **kwargs): super().__init__( name="control-sliders", orientation="h", spacing=8, **kwargs, ) brightness = Brightness.get_initial() if brightness.screen_brightness != -1: brightness_row = Box( orientation="h", spacing=0, h_expand=True, h_align="fill" ) brightness_row.add(BrightnessIcon()) brightness_row.add(BrightnessSlider()) self.add(brightness_row) volume_row = Box(orientation="h", spacing=0, h_expand=True, h_align="fill") volume_row.add(VolumeIcon()) volume_row.add(VolumeSlider()) self.add(volume_row) mic_row = Box(orientation="h", spacing=0, h_expand=True, h_align="fill") mic_row.add(MicIcon()) mic_row.add(MicSlider()) self.add(mic_row) self.show_all() class ControlSmall(Box): def __init__(self, **kwargs): brightness = Brightness.get_initial() children = [] if brightness.screen_brightness != -1: children.append(BrightnessSmall()) children.extend([VolumeSmall(), MicSmall()]) super().__init__( name="control-small", orientation="h" if not data.VERTICAL else "v", spacing=4, children=children, **kwargs, ) self.show_all()