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 TextBox _txtFilter; 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 = []; // Checked apps survive filtering (a filtered-out row keeps its check here). // pid → clean display name (used for the shared scope's Names). private readonly Dictionary _checked = new(); 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 (▶ = currently playing):", AutoSize = true, Location = new Point(12, 112), Visible = false, TabIndex = 4, }; _txtFilter = new TextBox { Location = new Point(12, 132), Size = new Size(360, 23), PlaceholderText = "Filter apps…", Visible = false, TabIndex = 5, }; _txtFilter.TextChanged += (_, _) => PopulateList(); _appList = new ListView { Location = new Point(12, 160), Size = new Size(360, 150), CheckBoxes = true, View = View.List, Visible = false, TabIndex = 6, 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, 320), Size = new Size(75, 27), TabIndex = 7, }; var btnCancel = new Button { Text = "&Cancel", DialogResult = DialogResult.Cancel, Location = new Point(297, 320), Size = new Size(75, 27), TabIndex = 8, }; btnShare.Click += OnShareClick; AcceptButton = btnShare; CancelButton = btnCancel; AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(384, 360); Controls.AddRange([_rdoAll, _rdoOnly, _rdoExcept, _chkExcludeSelf, _lblApps, _txtFilter, _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(); PopulateList(); } // Rebuild the visible rows from _apps + the current filter text, restoring check // state from _checked so a selection persists while the user filters. private void PopulateList() { string filter = _txtFilter.Text.Trim(); _suppressItemCheck = true; _appList.BeginUpdate(); _appList.Items.Clear(); foreach (var app in _apps) { if (filter.Length > 0 && app.DisplayName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0) continue; string marker = app.IsPlaying ? "▶ " : ""; _appList.Items.Add(new ListViewItem($"{marker}{app.DisplayName} (PID {app.Pid})") { Tag = app, Checked = _checked.ContainsKey(app.Pid), }); } _appList.EndUpdate(); _suppressItemCheck = false; } private void OnModeChanged(object? sender, EventArgs e) { bool showList = _rdoOnly.Checked || _rdoExcept.Checked; _lblApps.Visible = showList; _txtFilter.Visible = showList; _appList.Visible = showList; _lblApps.Text = _rdoExcept.Checked ? "App to exclude (everything else is shared):" : "Apps (▶ = currently playing):"; // 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; // Switching into exclude mode: collapse any multi-selection down to a single item. if (_rdoExcept.Checked) TrimCheckedToOne(); if (showList && _appList.Items.Count == 0) RefreshAppList(); else PopulateList(); // re-render markers/labels and restore check state } // 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) return; if (_appList.Items[e.Index].Tag is not AudioAppInfo app) return; if (e.NewValue == CheckState.Checked) { if (_rdoExcept.Checked) { // Radio behavior: clear every other visible check and the persisted set. _suppressItemCheck = true; foreach (ListViewItem item in _appList.Items) if (item.Index != e.Index && item.Checked) item.Checked = false; _suppressItemCheck = false; _checked.Clear(); } _checked[app.Pid] = app.DisplayName; } else { _checked.Remove(app.Pid); } } // Reduce the persisted selection to a single (first) entry — used when entering // exclude mode, whose single EXCLUDE slot can only target one process tree. private void TrimCheckedToOne() { if (_checked.Count <= 1) return; var first = _checked.First(); _checked.Clear(); _checked[first.Key] = first.Value; } private void OnShareClick(object? sender, EventArgs e) { if (_rdoAll.Checked) { ChosenScope = new EntireDesktop(_chkExcludeSelf.Checked); return; } if (_checked.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; } var pids = _checked.Keys.ToList(); var names = _checked.Values.ToList(); ChosenScope = _rdoOnly.Checked ? new OnlyApps(pids, names) : new AllExceptApps(pids, names); } }