"""DeviceCard — one card in the Output Devices / Input Devices tabs: MeterCard's usual icon+name/peak/meter/mute, plus a profile dropdown (pro-audio vs the plain stereo duplex profile, etc. — whatever the owning card advertises via `pactl list cards`) and a "Set Default" button. Shared between both tabs via the `kind` parameter ("sink" for outputs, "source" for inputs) — the only difference between an output and an input device, as far as pactl's verbs go, is which noun you say. """ from __future__ import annotations import gi gi.require_version("Gtk", "4.0") from gi.repository import Gtk # noqa: E402 from ui.card_base import MeterCard _MONITOR_SUFFIX = ".monitor" class DeviceCard(MeterCard): def __init__(self, backend, peaks, kind: str, item: dict, cards_by_id: dict[int, dict], default_name: str, set_default) -> None: self._cards_by_id = cards_by_id self._default_name = default_name self._set_default = set_default self._profile_names: list[str] = [] self._profile_guard = False super().__init__(backend, peaks, kind, item) def _build_extra(self, item: dict): box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) profile_col = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) profile_label = Gtk.Label(label="Profile", xalign=0.0) profile_label.add_css_class("sb-field-label") profile_col.append(profile_label) self._profile_dropdown = Gtk.DropDown() self._profile_dropdown.add_css_class("sb-device-dropdown") self._profile_dropdown.connect("notify::selected", self._on_profile_selected) profile_col.append(self._profile_dropdown) box.append(profile_col) self._default_btn = Gtk.ToggleButton(label="Set Default") self._default_btn.add_css_class("sb-default-btn") self._default_btn.connect("toggled", self._on_default_toggled) box.append(self._default_btn) self._sync_profile(item, self._cards_by_id) self._sync_default(item, self._default_name) return box def _update_extra(self, item: dict, cards_by_id: dict[int, dict], default_name: str) -> None: self._cards_by_id = cards_by_id self._default_name = default_name self._sync_profile(item, cards_by_id) self._sync_default(item, default_name) # -- profile dropdown ----------------------------------------------------------- def _owning_card(self, item: dict, cards_by_id: dict[int, dict]) -> dict | None: card_id = item.get("card") return cards_by_id.get(card_id) if card_id is not None else None def _sync_profile(self, item: dict, cards_by_id: dict[int, dict]) -> None: card = self._owning_card(item, cards_by_id) profiles = card.get("profiles") if card else [] if not profiles: self._profile_dropdown.set_sensitive(False) return self._profile_dropdown.set_sensitive(True) names = [p["name"] for p in profiles] if names != self._profile_names: self._profile_names = names self._profile_guard = True self._profile_dropdown.set_model( Gtk.StringList.new([p["description"] for p in profiles])) self._profile_guard = False active = card.get("active_profile") if card else None idx = names.index(active) if active in names else None if idx is not None and self._profile_dropdown.get_selected() != idx: self._profile_guard = True self._profile_dropdown.set_selected(idx) self._profile_guard = False def _on_profile_selected(self, dropdown: Gtk.DropDown, _pspec) -> None: if self._profile_guard: return card = self._owning_card(self._item, self._cards_by_id) if card is None: return idx = dropdown.get_selected() if 0 <= idx < len(self._profile_names): self._backend.set_card_profile(card["id"], self._profile_names[idx]) # -- default device --------------------------------------------------------------- def _sync_default(self, item: dict, default_name: str) -> None: is_default = item.get("name") == default_name self._default_btn.handler_block_by_func(self._on_default_toggled) self._default_btn.set_active(is_default) self._default_btn.handler_unblock_by_func(self._on_default_toggled) self._default_btn.set_label("Default" if is_default else "Set Default") self._default_btn.set_sensitive(not is_default) def _on_default_toggled(self, btn: Gtk.ToggleButton) -> None: if btn.get_active(): self._set_default(self._item.get("name")) def _peak_target(self, item: dict) -> list[str] | None: name = item.get("name") if not name: return None if self._kind == "sink": return [f"--device={name}{_MONITOR_SUFFIX}"] return [f"--device={name}"]