Files
SDG-DRIFT/Ax-Shell/modules/controls.py
T
2026-06-03 21:32:45 +02:00

873 lines
30 KiB
Python

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()