Dotfiles/desktopenvs/hyprlua/audio-panel/window.py

203 lines
7.8 KiB
Python

"""The popup: a content-sized floating layer-shell panel anchored top-right,
matching transmitter-panel/astro-menu's window.py idiom — dismissed with the
launcher toggle, Esc, or the ✕ button, no click-outside-to-close.
Anchored top-right (not centred) so it sits roughly under the volume control
that invokes it — station-bar's volume badge and every eww variant's volume
slider all live in the right-hand zone of their bar. This is a fixed
approximation, not a live "appear under this exact widget" position: the
wlr-layer-shell protocol only supports anchor+margin-to-screen-edge
placement, not popup-relative-to-another-surface placement, and the panel is
a separate process from whichever bar invoked it, so it has no way to learn
that bar's precise pixel geometry. Tune RIGHT_MARGIN below if it drifts from
whichever bar/build you're running against.
Gtk.Window (layer TOP, anchored TOP+RIGHT, height/width = content)
└ Gtk.Overlay
main : .sb-panel (title, tab switcher, Gtk.Stack of the four tabs)
over : hologram overlay
over : ✕ close button (top-right)
Owns the single AudioBackend and PeakMonitorManager shared by every tab —
one `pactl subscribe` process and one live-metering manager for the whole
panel, not one per tab.
"""
from __future__ import annotations
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Gdk", "4.0")
gi.require_version("Gtk4LayerShell", "1.0")
from gi.repository import Gdk, Gtk # noqa: E402
from gi.repository import Gtk4LayerShell as LayerShell # noqa: E402
import config
from backend.peaklevel import PeakMonitorManager
from backend.pipewire import AudioBackend
from lib.hologram import HologramOverlay
from ui.applications_tab import ApplicationsTab
from ui.general_tab import GeneralTab
from ui.input_tab import InputTab
from ui.output_tab import OutputTab
from ui.tabs import TabSwitcher
PANEL_WIDTH = 460
TOP_MARGIN = 28
RIGHT_MARGIN = 160 # approximate horizontal offset to sit under the volume control
class AudioPanelWindow(Gtk.ApplicationWindow):
def __init__(self, app: Gtk.Application) -> None:
super().__init__(application=app)
self.set_name("audio-panel-window")
self.add_css_class("sb-window")
self.set_decorated(False)
self._init_layer_shell()
self._backend = AudioBackend(self._on_backend_changed)
self._peaks = PeakMonitorManager()
self.root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
self.root.set_name("panel-root")
self.root.add_css_class("sb-panel")
self.root.set_size_request(PANEL_WIDTH, -1)
header = Gtk.CenterBox()
header.add_css_class("sb-header")
title = Gtk.Label(label="Audio Panel", xalign=0.0)
title.add_css_class("sb-title")
header.set_start_widget(title)
self.root.append(header)
self._stack = Gtk.Stack()
self._stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
self._stack.set_transition_duration(150)
self._applications_tab = ApplicationsTab(self._backend, self._peaks)
self._output_tab = OutputTab(self._backend, self._peaks)
self._input_tab = InputTab(self._backend, self._peaks)
self._general_tab = GeneralTab(self._backend, on_hologram_changed=self._on_hologram_setting_changed)
self._tabs = {
"applications": self._applications_tab,
"output": self._output_tab,
"input": self._input_tab,
"general": self._general_tab,
}
tab_order = [
("applications", "Applications", self._applications_tab),
("input", "Input Devices", self._input_tab),
("output", "Output Devices", self._output_tab),
("general", "General Settings", self._general_tab),
]
for name, label, widget in tab_order:
self._stack.add_titled(widget, name, label)
self._active_tab_name = tab_order[0][0]
self._tabswitcher = TabSwitcher(tab_order, self._stack, self._on_tab_changed)
self.root.append(self._tabswitcher)
self.root.append(self._stack)
overlay = Gtk.Overlay()
overlay.set_child(self.root)
self._hologram = HologramOverlay(enabled=config.hologram_enabled(), fade_widget=self.root)
overlay.add_overlay(self._hologram.widget)
close = Gtk.Button(label="")
close.add_css_class("close-btn")
close.set_halign(Gtk.Align.END)
close.set_valign(Gtk.Align.START)
close.connect("clicked", lambda *_a: self.hide_panel())
overlay.add_overlay(close)
self.set_child(overlay)
key = Gtk.EventControllerKey()
key.connect("key-pressed", self._on_key)
self.add_controller(key)
self._last_tick: float | None = None
self._tick_id: int | None = None
self.set_visible(False)
# -- layer shell --------------------------------------------------------------
def _init_layer_shell(self) -> None:
LayerShell.init_for_window(self)
LayerShell.set_layer(self, LayerShell.Layer.TOP)
LayerShell.set_namespace(self, "audio-panel")
LayerShell.set_keyboard_mode(self, LayerShell.KeyboardMode.ON_DEMAND)
LayerShell.set_anchor(self, LayerShell.Edge.TOP, True)
LayerShell.set_anchor(self, LayerShell.Edge.RIGHT, True)
LayerShell.set_margin(self, LayerShell.Edge.TOP, TOP_MARGIN)
LayerShell.set_margin(self, LayerShell.Edge.RIGHT, RIGHT_MARGIN)
# -- visibility -----------------------------------------------------------------
def show_panel(self) -> None:
self.set_visible(True)
self.present()
if self._hologram.enabled and self._tick_id is None:
self._tick_id = self.add_tick_callback(self._on_tick)
self._hologram.start_intro()
self._refresh_all()
self._tabs[self._active_tab_name].activate()
def hide_panel(self) -> None:
self._tabs[self._active_tab_name].deactivate()
self._peaks.stop_all()
if self._hologram.enabled and self._tick_id is not None:
self._hologram.start_outro(self._finish_hide)
else:
self._finish_hide()
def _finish_hide(self) -> None:
self.set_visible(False)
if self._tick_id is not None:
self.remove_tick_callback(self._tick_id)
self._tick_id = None
self._last_tick = None
def toggle(self) -> None:
if self.get_visible():
self.hide_panel()
else:
self.show_panel()
# -- tabs ---------------------------------------------------------------------
def _on_tab_changed(self, name: str) -> None:
if name == self._active_tab_name:
return
self._tabs[self._active_tab_name].deactivate()
self._active_tab_name = name
self._tabs[name].activate()
def _refresh_all(self) -> None:
for tab in self._tabs.values():
tab.refresh()
def _on_backend_changed(self) -> None:
if not self.get_visible():
return
self._refresh_all()
def _on_hologram_setting_changed(self, enabled: bool) -> None:
self._hologram.enabled = enabled
if enabled and self.get_visible() and self._tick_id is None:
self._tick_id = self.add_tick_callback(self._on_tick)
# -- hologram tick --------------------------------------------------------------
def _on_tick(self, _widget, frame_clock) -> bool:
now = frame_clock.get_frame_time() / 1_000_000
dt = 0.0 if self._last_tick is None else max(0.0, now - self._last_tick)
self._last_tick = now
self._hologram.tick(dt)
return True
def _on_key(self, _c, keyval, _kc, _state) -> bool:
if keyval == Gdk.KEY_Escape:
self.hide_panel()
return True
return False