190 lines
6.9 KiB
Python
190 lines
6.9 KiB
Python
"""The popup: a content-sized floating layer-shell panel anchored top-centre,
|
|
matching transmitter-panel/astro-menu's window.py idiom — dismissed with the
|
|
launcher toggle, Esc, or the ✕ button, no click-outside-to-close.
|
|
|
|
Gtk.Window (layer TOP, anchored TOP -> horizontally centred, height = 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 = 900
|
|
EDGE_MARGIN = 28
|
|
|
|
|
|
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_margin(self, LayerShell.Edge.TOP, EDGE_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
|