90 lines
3.6 KiB
Python
90 lines
3.6 KiB
Python
"""ProfileCard — one card in the Cards tab: a sound *card* (the physical or
|
|
virtual device behind one or more sinks/sources — onboard audio, a USB
|
|
headset, HDMI, a Bluetooth device) and its profile dropdown.
|
|
|
|
Profile is a card-level setting, not a per-device one — pavucontrol puts it
|
|
in its own "Configuration" tab for the same reason: a single card can back
|
|
multiple sinks/sources at once, and switching its profile can make some of
|
|
them appear or disappear entirely (e.g. "Analog Stereo Duplex" exposes both
|
|
an output and an input from the same card; "Analog Stereo Output" drops the
|
|
input side; "Off" drops both; "Pro Audio" breaks the card into raw individual
|
|
ports instead of one grouped stereo pair). Output/Input Devices used to carry
|
|
their own copy of this dropdown per-device, which was confusing when one
|
|
card backed a device in both tabs — this replaces that."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import gi
|
|
|
|
gi.require_version("Gtk", "4.0")
|
|
from gi.repository import Gtk # noqa: E402
|
|
|
|
|
|
class ProfileCard(Gtk.Box):
|
|
def __init__(self, backend, item: dict) -> None:
|
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
|
self.add_css_class("sb-card")
|
|
self._backend = backend
|
|
self._id = item["id"]
|
|
self._profile_names: list[str] = []
|
|
self._guard = False
|
|
|
|
head = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
|
head.add_css_class("sb-card-head")
|
|
icon = Gtk.Image.new_from_icon_name("audio-card")
|
|
icon.set_pixel_size(28)
|
|
head.append(icon)
|
|
self._name_lbl = Gtk.Label(xalign=0.0)
|
|
self._name_lbl.add_css_class("sb-card-title")
|
|
self._name_lbl.set_ellipsize(3) # Pango.EllipsizeMode.END
|
|
self._name_lbl.set_max_width_chars(20)
|
|
head.append(self._name_lbl)
|
|
self.append(head)
|
|
|
|
label = Gtk.Label(label="Profile", xalign=0.0)
|
|
label.add_css_class("sb-field-label")
|
|
self.append(label)
|
|
self._dropdown = Gtk.DropDown()
|
|
self._dropdown.add_css_class("sb-device-dropdown")
|
|
self._dropdown.connect("notify::selected", self._on_selected)
|
|
self.append(self._dropdown)
|
|
|
|
self.update(item)
|
|
|
|
def update(self, item: dict) -> None:
|
|
self._item = item
|
|
name = item.get("description") or item.get("name") or f"#{item.get('id')}"
|
|
self._name_lbl.set_label(name)
|
|
self._name_lbl.set_tooltip_text(name)
|
|
|
|
profiles = item.get("profiles") or []
|
|
names = [p["name"] for p in profiles]
|
|
if names != self._profile_names:
|
|
self._profile_names = names
|
|
self._guard = True
|
|
self._dropdown.set_model(Gtk.StringList.new([p["description"] for p in profiles]))
|
|
self._guard = False
|
|
self._dropdown.set_sensitive(bool(profiles))
|
|
|
|
active = item.get("active_profile")
|
|
idx = names.index(active) if active in names else None
|
|
if idx is not None and self._dropdown.get_selected() != idx:
|
|
self._guard = True
|
|
self._dropdown.set_selected(idx)
|
|
self._guard = False
|
|
|
|
def _on_selected(self, dropdown: Gtk.DropDown, _pspec) -> None:
|
|
if self._guard:
|
|
return
|
|
idx = dropdown.get_selected()
|
|
if 0 <= idx < len(self._profile_names):
|
|
self._backend.set_card_profile(self._id, self._profile_names[idx])
|
|
|
|
# ScrollRow's diff-sync calls these on every card unconditionally (see
|
|
# scroll_row.py) — cards have no live audio to meter, so these are no-ops.
|
|
def start_peak(self) -> None:
|
|
pass
|
|
|
|
def stop_peak(self) -> None:
|
|
pass
|