using VoiceCat.App.Audio; namespace VoiceCat.App.Forms; /// /// Modal dialog for selecting which apps' audio to share. /// Returns the chosen via , /// or if the user dismissed without sharing. /// public sealed class AppAudioPickerDialog : Form { private readonly RadioButton _rdoAll; private readonly RadioButton _rdoOnly; private readonly RadioButton _rdoExcept; private readonly CheckBox _chkExcludeSelf; private readonly ListView _appList; private readonly Label _lblApps; // Snapshot taken when the dialog opens (refresh on open, not on every check change). private IReadOnlyList _apps = []; public AppAudioScope? ChosenScope { get; private set; } public AppAudioPickerDialog() { // ── Radio buttons ─────────────────────────────────────────────────── _rdoAll = new RadioButton { Text = "&Entire desktop", Checked = true, Location = new Point(12, 12), Size = new Size(360, 20), TabIndex = 0, }; _rdoOnly = new RadioButton { Text = "&Only selected apps", Location = new Point(12, 36), Size = new Size(360, 20), TabIndex = 1, }; _rdoExcept = new RadioButton { Text = "All apps e&xcept selected", Location = new Point(12, 60), Size = new Size(360, 20), TabIndex = 2, }; _rdoAll.CheckedChanged += OnModeChanged; _rdoOnly.CheckedChanged += OnModeChanged; _rdoExcept.CheckedChanged += OnModeChanged; // ── Self-exclude (echo prevention) ─────────────────────────────────── // Captures the whole desktop minus VoiceCat's own playback, so the channel // doesn't echo back the voices you're already hearing. Only applies to the // entire-desktop mode (the per-app modes already exclude this app's tree). _chkExcludeSelf = new CheckBox { Text = "E&xclude VoiceCat's own audio (prevents echo)", Checked = true, Location = new Point(28, 84), Size = new Size(344, 20), TabIndex = 3, }; // ── App list ──────────────────────────────────────────────────────── _lblApps = new Label { Text = "Apps with active audio sessions:", AutoSize = true, Location = new Point(12, 112), Visible = false, TabIndex = 4, }; _appList = new ListView { Location = new Point(12, 134), Size = new Size(360, 158), CheckBoxes = true, View = View.List, Visible = false, TabIndex = 5, FullRowSelect = true, }; // Exclude mode supports only one target process tree (WASAPI EXCLUDE takes a single // PID), so enforce a single check while "All apps except selected" is active. _appList.ItemCheck += OnAppItemCheck; // ── Buttons ───────────────────────────────────────────────────────── var btnShare = new Button { Text = "&Share", DialogResult = DialogResult.OK, Location = new Point(216, 308), Size = new Size(75, 27), TabIndex = 6, }; var btnCancel = new Button { Text = "&Cancel", DialogResult = DialogResult.Cancel, Location = new Point(297, 308), Size = new Size(75, 27), TabIndex = 7, }; btnShare.Click += OnShareClick; AcceptButton = btnShare; CancelButton = btnCancel; AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(384, 348); Controls.AddRange([_rdoAll, _rdoOnly, _rdoExcept, _chkExcludeSelf, _lblApps, _appList, btnShare, btnCancel]); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false; StartPosition = FormStartPosition.CenterParent; Text = "Share App Audio"; } protected override void OnLoad(EventArgs e) { base.OnLoad(e); RefreshAppList(); } private void RefreshAppList() { _apps = AudioSessionEnumerator.GetAudioApps(); _appList.Items.Clear(); foreach (var app in _apps) _appList.Items.Add(new ListViewItem($"{app.DisplayName} (PID {app.Pid})") { Tag = app.Pid }); } private void OnModeChanged(object? sender, EventArgs e) { bool showList = _rdoOnly.Checked || _rdoExcept.Checked; _lblApps.Visible = showList; _appList.Visible = showList; _lblApps.Text = _rdoExcept.Checked ? "App to exclude (everything else is shared):" : "Apps with active audio sessions:"; // Self-exclude only applies to entire-desktop; the per-app modes already exclude // this app's own tree (Include) or spend the single EXCLUDE slot on the chosen app. _chkExcludeSelf.Enabled = _rdoAll.Checked; if (showList && _appList.Items.Count == 0) RefreshAppList(); // Switching into exclude mode: collapse any multi-selection down to a single item. if (_rdoExcept.Checked) KeepSingleCheck(firstCheckedOnly: true); } // In exclude mode the list behaves like radio buttons: checking one item clears the rest. private bool _suppressItemCheck; private void OnAppItemCheck(object? sender, ItemCheckEventArgs e) { if (_suppressItemCheck || !_rdoExcept.Checked || e.NewValue != CheckState.Checked) return; _suppressItemCheck = true; foreach (ListViewItem item in _appList.Items) if (item.Index != e.Index && item.Checked) item.Checked = false; _suppressItemCheck = false; } // Leave at most one item checked (the first), unchecking the rest. private void KeepSingleCheck(bool firstCheckedOnly) { _suppressItemCheck = true; bool kept = false; foreach (ListViewItem item in _appList.Items) { if (!item.Checked) continue; if (firstCheckedOnly && !kept) { kept = true; continue; } item.Checked = false; } _suppressItemCheck = false; } private void OnShareClick(object? sender, EventArgs e) { if (_rdoAll.Checked) { ChosenScope = new EntireDesktop(_chkExcludeSelf.Checked); return; } var checkedPids = new List(); var checkedNames = new List(); foreach (ListViewItem item in _appList.CheckedItems) { if (item.Tag is int pid) { checkedPids.Add(pid); checkedNames.Add(item.Text); } } if (checkedPids.Count == 0) { MessageBox.Show( "Select at least one app, or choose 'Entire desktop'.", "No apps selected", MessageBoxButtons.OK, MessageBoxIcon.Warning); DialogResult = DialogResult.None; // prevent close return; } ChosenScope = _rdoOnly.Checked ? new OnlyApps(checkedPids, checkedNames) : new AllExceptApps(checkedPids, checkedNames); } }