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);
}
}