"""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