Service config dialog: rebuilt to match the main window exactly

Local checkpoint - NOT for public release. Ed flagged the dialog as sloppy - lists
not announcing, wrong tab order, leftover Alt+4/5/6, "Standard/Tight" instead of the
real labels, and not looking like the main window.

- Screen-reader parity: new CheckedListAccessibility.Wire (factored from the main
  window's WireCheckedListAccessibility) drives every checked list - announces
  "checked, <item>. Item N of M. Press Space to toggle." on focus and arrow, plus
  first-letter nav that never toggles. This was the core miss (lists not announcing).
- Layout parity: uses the house FormLayoutRows rows + status labels, QuietTabControl,
  AccessibleCheckBox, MnemonicLabel, and the app icon - so it reads/looks like a real tab.
- Tab order now mirrors the main window: Connectivity, then Audio send, then Audio profile.
- Alt keys renumbered for the dialog (were lifted verbatim from the I/O tab): send tab
  1-5, connectivity 1-2, profile keeps the main window's C/P/D.
- Exact main-window labels copied verbatim: codec ("PCM 48K 24 bit - uncompressed", etc.),
  packet size ("Standard (5 ms PCM, 10/20 ms Opus)" / "Small (2.5 ms ...)"), and the
  lock-to-audio-clock text + accessible description.

Gate 25/25 (dialog passes the accessibility audit: names + unique mnemonics + tab order).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-12 20:21:48 +01:00
co-authored by Claude Opus 4.8
parent 9a7bd6c9e8
commit 37f28d6c91
2 changed files with 176 additions and 94 deletions
@@ -0,0 +1,90 @@
using System.Windows.Forms;
namespace RemSound.App;
/// <summary>
/// Shared screen-reader wiring for a <see cref="CheckedListBox"/> — the exact behaviour the main
/// window's device lists use, factored out so dialogs (e.g. the service profile editor) announce
/// identically instead of each rolling its own. On focus and on arrow-key move it writes a spoken
/// status ("checked, VLC. Item 1 of 3. Press Space to toggle.") into both the list's
/// AccessibleDescription and a companion status label, so NVDA reads the item AND its checked state.
/// Also adds first-letter navigation that never accidentally toggles a check.
/// </summary>
internal static class CheckedListAccessibility
{
public static void Wire(CheckedListBox list, Label statusLabel, string itemKind)
{
var lastIndex = 0;
void Update(int? overrideIndex = null, bool? overrideChecked = null)
=> SetStatus(list, statusLabel, itemKind, overrideIndex, overrideChecked);
void RestoreFocus()
{
if (list.Items.Count == 0) { Update(); return; }
var target = list.SelectedIndex >= 0 ? list.SelectedIndex : Math.Clamp(lastIndex, 0, list.Items.Count - 1);
if (list.SelectedIndex != target) list.SelectedIndex = target;
lastIndex = target;
Update();
}
list.SelectedIndexChanged += (_, _) =>
{
if (list.SelectedIndex >= 0) lastIndex = list.SelectedIndex;
Update();
};
list.ItemCheck += (_, e) => Update(e.Index, e.NewValue == CheckState.Checked);
list.Enter += (_, _) => RestoreFocus();
list.GotFocus += (_, _) => RestoreFocus();
list.MouseDown += (_, args) =>
{
var index = list.IndexFromPoint(args.Location);
if (index >= 0) { list.SelectedIndex = index; lastIndex = index; }
};
// First-letter navigation that highlights the matching item without ever toggling its check
// (the default CheckedListBox key handling has been seen to toggle on a unique single-letter
// prefix). Spacebar still falls through so Space toggles as normal.
list.KeyDown += (_, args) =>
{
if (args.Modifiers != Keys.None) return;
char ch;
if (args.KeyCode >= Keys.A && args.KeyCode <= Keys.Z) ch = (char)('a' + (args.KeyCode - Keys.A));
else if (args.KeyCode >= Keys.D0 && args.KeyCode <= Keys.D9) ch = (char)('0' + (args.KeyCode - Keys.D0));
else if (args.KeyCode >= Keys.NumPad0 && args.KeyCode <= Keys.NumPad9) ch = (char)('0' + (args.KeyCode - Keys.NumPad0));
else return;
var startIdx = list.SelectedIndex < 0 ? 0 : list.SelectedIndex + 1;
for (var offset = 0; offset < list.Items.Count; offset++)
{
var idx = (startIdx + offset) % list.Items.Count;
var text = list.Items[idx]?.ToString() ?? string.Empty;
if (text.Length > 0 && char.ToLowerInvariant(text[0]) == ch) { list.SelectedIndex = idx; break; }
}
args.Handled = true;
args.SuppressKeyPress = true;
};
Update();
}
// Exact copy of MainForm.UpdateCheckedListStatus so the spoken text is word-for-word identical.
private static void SetStatus(CheckedListBox list, Label statusLabel, string itemKind, int? overrideIndex, bool? overrideChecked)
{
if (list.Items.Count == 0)
{
var emptyText = $"No {itemKind}s available.";
statusLabel.Text = emptyText;
list.AccessibleDescription = emptyText;
return;
}
var index = overrideIndex ?? (list.SelectedIndex >= 0 ? list.SelectedIndex : 0);
index = Math.Clamp(index, 0, list.Items.Count - 1);
var isChecked = overrideChecked ?? list.GetItemChecked(index);
var checkedText = isChecked ? "checked" : "not checked";
var itemText = list.Items[index]?.ToString() ?? itemKind;
var text = $"{checkedText}, {itemText}. Item {index + 1} of {list.Items.Count}. Press Space to toggle.";
statusLabel.Text = text;
list.AccessibleDescription = text;
statusLabel.AccessibleDescription = text;
}
}
+86 -94
View File
@@ -5,39 +5,41 @@ namespace RemSound.App;
/// <summary> /// <summary>
/// The "Configure RemSound service profile" dialog: a self-contained, modal editor for the send-only /// The "Configure RemSound service profile" dialog: a self-contained, modal editor for the send-only
/// service profile. Three tabs — Audio send, Audio profile, Connectivity — plus a button row of /// service profile, built to match the main window — same house controls, same layout rows, and the
/// Save and Close / Cancel / Additional options. Being modal, there are no menus and no profile /// same screen-reader wiring on the lists (via <see cref="CheckedListAccessibility"/>), so it reads and
/// switching to worry about. It edits a <see cref="Profile"/> clone; nothing is persisted until the /// behaves exactly like the real tabs. Tab order mirrors the main window: Connectivity, then Audio send,
/// caller acts on an OK result. Send-only and WASAPI-only: no receive controls, no ASIO, and no /// then Audio profile. Send-only and WASAPI-only: no receive controls, no ASIO, no "Send my audio"
/// "Send my audio" toggle (it always sends). /// toggle. Edits a <see cref="Profile"/> clone; nothing is persisted until the caller acts on OK.
/// </summary> /// </summary>
internal sealed class ServiceProfileDialog : Form internal sealed class ServiceProfileDialog : Form
{ {
private readonly Profile working; private readonly Profile working;
private readonly QuietTabControl tabs = new() { Dock = DockStyle.Fill }; private readonly QuietTabControl tabs = new() { Dock = DockStyle.Fill };
// --- Audio send tab ---
private readonly ListBox sendModeList = new() { Width = 460, Height = 38, IntegralHeight = false, AccessibleName = "How to send WASAPI audio (Alt+6)" };
private readonly CheckedListBox outputsList = new() { CheckOnClick = true, Width = 460, Height = 110, AccessibleName = "WASAPI audio outputs to send (Alt+4)" };
private readonly AccessibleCheckBox sendAllAppsBox = new() { Text = "Send all applications (Alt+&7)", AccessibleName = "Send all applications", AutoSize = true, Checked = true };
private readonly CheckedListBox appsList = new() { CheckOnClick = true, Width = 460, Height = 110, AccessibleName = "Applications to send (Alt+8)" };
private readonly CheckedListBox inputsList = new() { CheckOnClick = true, Width = 460, Height = 90, AccessibleName = "WASAPI audio inputs to send (Alt+5)" };
private MnemonicLabel? sendModeLabel, outputsLabel, appsLabel, inputsLabel;
// --- Audio profile tab ---
private readonly ComboBox codecBox = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 360, AccessibleName = "Audio codec (Alt+C)" };
private readonly ComboBox sendRateBox = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 360, AccessibleName = "Send rate (Alt+E)" };
private readonly AccessibleCheckBox tightLatencyBox = new() { Text = "&Lock to audio clock — steadier timing, slightly higher latency (Alt+L)", AccessibleName = "Lock to audio clock", AutoSize = true };
// --- Connectivity tab --- // --- Connectivity tab ---
private readonly ListBox peersList = new() { Width = 360, Height = 120, AccessibleName = "Peers to send to (Alt+P)" }; private readonly ListBox peersList = new() { Width = 460, Height = 120, AccessibleName = "Peers to send to (Alt+1)" };
private readonly TextBox addPeerBox = new() { Width = 260, AccessibleName = "Add a peer address or hostname (Alt+A)" }; private readonly TextBox addPeerBox = new() { Width = 300, AccessibleName = "Add a peer — IP address or hostname (Alt+2)" };
private readonly Button addPeerButton = new() { Text = "A&dd", AutoSize = true, AccessibleName = "Add peer" }; private readonly Button addPeerButton = new() { Text = "A&dd", AutoSize = true, AccessibleName = "Add peer" };
private readonly Button removePeerButton = new() { Text = "&Remove", AutoSize = true, AccessibleName = "Remove selected peer" }; private readonly Button removePeerButton = new() { Text = "&Remove", AutoSize = true, AccessibleName = "Remove selected peer" };
private readonly Button passwordButton = new() { Text = "Set pass&word...", AutoSize = true, AccessibleName = "Set the service profile password" }; private readonly Button passwordButton = new() { Text = "Set pass&word...", AutoSize = true, AccessibleName = "Set the service profile password" };
private readonly Label passwordStatus = new() { AutoSize = true }; private readonly Label passwordStatus = new() { AutoSize = true };
// --- Audio send tab ---
private readonly ListBox sendModeList = new() { Width = 460, Height = 40, IntegralHeight = false, AccessibleName = "How to send WASAPI audio (Alt+1)" };
private readonly CheckedListBox outputsList = new() { CheckOnClick = true, Width = 460, Height = 110, AccessibleName = "WASAPI audio outputs to send (Alt+2)" };
private readonly Label outputsStatus = new() { AutoSize = true, Text = "No output device selected." };
private readonly AccessibleCheckBox sendAllAppsBox = new() { Text = "Send all applications (Alt+&3)", AccessibleName = "Send all applications", AutoSize = true, Checked = true };
private readonly CheckedListBox appsList = new() { CheckOnClick = true, Width = 460, Height = 110, AccessibleName = "Applications to send (Alt+4)" };
private readonly Label appsStatus = new() { AutoSize = true, Text = "No application selected." };
private readonly CheckedListBox inputsList = new() { CheckOnClick = true, Width = 460, Height = 90, AccessibleName = "WASAPI audio inputs to send (Alt+5)" };
private readonly Label inputsStatus = new() { AutoSize = true, Text = "No input device selected." };
private MnemonicLabel? sendModeLabel, outputsLabel, appsLabel, inputsLabel;
// --- Audio profile tab ---
private readonly ComboBox codecBox = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 360, AccessibleName = "Audio codec (Alt+C)" };
private readonly ListBox sendRateBox = new() { Width = 360, Height = 40, IntegralHeight = false, AccessibleName = "Packet size (Alt+P)" };
private readonly AccessibleCheckBox tightLatencyBox = new() { AutoSize = true };
// --- Button row --- // --- Button row ---
private readonly Button saveButton = new() { Text = "&Save and Close", AutoSize = true, DialogResult = DialogResult.OK }; private readonly Button saveButton = new() { Text = "&Save and Close", AutoSize = true, DialogResult = DialogResult.OK };
private readonly Button cancelButton = new() { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel }; private readonly Button cancelButton = new() { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel };
@@ -45,11 +47,7 @@ internal sealed class ServiceProfileDialog : Form
private bool suppressAppEvents; private bool suppressAppEvents;
/// <summary>The edited profile (valid after an OK result).</summary>
public Profile Result => working; public Profile Result => working;
/// <summary>Whether the service should write its own log — machine-wide, so it's returned separately
/// from the profile. Seeded from AppConfig; the caller writes it back to AppConfig on OK.</summary>
public bool ServiceLoggingEnabled { get; private set; } public bool ServiceLoggingEnabled { get; private set; }
public ServiceProfileDialog(Profile current, bool serviceLoggingEnabled) public ServiceProfileDialog(Profile current, bool serviceLoggingEnabled)
@@ -65,12 +63,14 @@ internal sealed class ServiceProfileDialog : Form
ShowInTaskbar = false; ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterParent; StartPosition = FormStartPosition.CenterParent;
KeyPreview = true; KeyPreview = true;
ClientSize = new Size(540, 560); ClientSize = new Size(560, 580);
AccessibleName = "Configure RemSound service profile"; AccessibleName = "Configure RemSound service profile";
if (Theme.AppIcon is { } icon) Icon = icon;
// Tabs in the main window's order: Connectivity, Audio send, Audio profile.
BuildConnectivityTab();
BuildAudioSendTab(); BuildAudioSendTab();
BuildAudioProfileTab(); BuildAudioProfileTab();
BuildConnectivityTab();
BuildButtonRow(); BuildButtonRow();
LoadFromProfile(); LoadFromProfile();
@@ -85,20 +85,47 @@ internal sealed class ServiceProfileDialog : Form
// ---------------- layout ---------------- // ---------------- layout ----------------
private TableLayoutPanel NewColumn() => new() private static TableLayoutPanel NewPanel()
{ {
Dock = DockStyle.Fill, var panel = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2, AutoScroll = true, Padding = new Padding(12) };
ColumnCount = 2, panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
AutoScroll = true, panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
Padding = new Padding(10), return panel;
}; }
private void BuildConnectivityTab()
{
var page = new TabPage("Connectivity");
var panel = NewPanel();
AddListRow(panel, 0, "Peers to send to (Alt+&1)", peersList);
var addLabel = new MnemonicLabel { Text = "Add a peer — IP address or hostname (Alt+&2)", AutoSize = true, Anchor = AnchorStyles.Left, MnemonicTarget = addPeerBox };
panel.Controls.Add(addLabel, 0, 1);
var addRow = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill };
addRow.Controls.Add(addPeerBox);
addRow.Controls.Add(addPeerButton);
addRow.Controls.Add(removePeerButton);
panel.Controls.Add(addRow, 1, 1);
addPeerButton.Click += (_, _) => AddPeerFromBox();
removePeerButton.Click += (_, _) => { if (peersList.SelectedItem is string s) peersList.Items.Remove(s); };
addPeerBox.KeyDown += (_, e) => { if (e.KeyCode == Keys.Enter) { AddPeerFromBox(); e.SuppressKeyPress = true; } };
var pwWrap = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill };
pwWrap.Controls.Add(passwordButton);
pwWrap.Controls.Add(passwordStatus);
panel.Controls.Add(pwWrap, 1, 2);
passwordButton.Click += (_, _) => SetPassword();
page.Controls.Add(panel);
tabs.TabPages.Add(page);
}
private void BuildAudioSendTab() private void BuildAudioSendTab()
{ {
var page = new TabPage("Audio send"); var page = new TabPage("Audio send");
var panel = NewColumn(); var panel = NewPanel();
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
sendModeList.Items.Add("Send whole audio devices"); sendModeList.Items.Add("Send whole audio devices");
sendModeList.Items.Add("Send specific applications"); sendModeList.Items.Add("Send specific applications");
@@ -106,13 +133,17 @@ internal sealed class ServiceProfileDialog : Form
sendModeList.SelectedIndexChanged += (_, _) => { if (!suppressAppEvents) ApplySendModeVisibility(); }; sendModeList.SelectedIndexChanged += (_, _) => { if (!suppressAppEvents) ApplySendModeVisibility(); };
sendAllAppsBox.CheckedChanged += (_, _) => { if (!suppressAppEvents) ApplySendModeVisibility(); }; sendAllAppsBox.CheckedChanged += (_, _) => { if (!suppressAppEvents) ApplySendModeVisibility(); };
sendModeLabel = AddListRow(panel, 0, "How to send WASAPI audio (Alt+&6)", sendModeList); sendModeLabel = AddListRow(panel, 0, "How to send WASAPI audio (Alt+&1)", sendModeList);
outputsLabel = AddListRow(panel, 1, "WASAPI audio outputs to send (Alt+&4)", outputsList); outputsLabel = FormLayoutRows.AddCheckedListRow(panel, 1, "WASAPI audio outputs to send (Alt+&2)", outputsList, outputsStatus, l => l.Focus());
var allAppsWrap = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill }; var allAppsWrap = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill };
allAppsWrap.Controls.Add(sendAllAppsBox); allAppsWrap.Controls.Add(sendAllAppsBox);
panel.Controls.Add(allAppsWrap, 1, 2); panel.Controls.Add(allAppsWrap, 1, 2);
appsLabel = AddListRow(panel, 3, "Applications to send (Alt+&8)", appsList); appsLabel = FormLayoutRows.AddCheckedListRow(panel, 3, "Applications to send (Alt+&4)", appsList, appsStatus, l => l.Focus());
inputsLabel = AddListRow(panel, 4, "WASAPI audio inputs to send (Alt+&5)", inputsList); inputsLabel = FormLayoutRows.AddCheckedListRow(panel, 4, "WASAPI audio inputs to send (Alt+&5)", inputsList, inputsStatus, l => l.Focus());
CheckedListAccessibility.Wire(outputsList, outputsStatus, "output device");
CheckedListAccessibility.Wire(appsList, appsStatus, "application");
CheckedListAccessibility.Wire(inputsList, inputsStatus, "input device");
page.Controls.Add(panel); page.Controls.Add(panel);
tabs.TabPages.Add(page); tabs.TabPages.Add(page);
@@ -121,20 +152,22 @@ internal sealed class ServiceProfileDialog : Form
private void BuildAudioProfileTab() private void BuildAudioProfileTab()
{ {
var page = new TabPage("Audio profile"); var page = new TabPage("Audio profile");
var panel = NewColumn(); var panel = NewPanel();
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
// Exact same codec choices, packet-size items and lock-to-clock label as the main window.
codecBox.Items.AddRange(new object[] codecBox.Items.AddRange(new object[]
{ {
new CodecChoice("Uncompressed PCM (best quality, most bandwidth)", AudioTransportCodec.Pcm, 480), new CodecChoice("PCM 48K 24 bit — uncompressed", AudioTransportCodec.Pcm, 0),
new CodecChoice("Opus broadcast quality (20 ms, loss tolerant)", AudioTransportCodec.Opus, 960), new CodecChoice("Opus, broadcast quality loss tolerant", AudioTransportCodec.Opus, 960),
new CodecChoice("Opus live latency (2.5 ms, for jamming)", AudioTransportCodec.Opus, 120), new CodecChoice("Opus, live latency for jamming and monitoring", AudioTransportCodec.Opus, 120),
}); });
sendRateBox.Items.AddRange(new object[] { SendRate.Standard, SendRate.Tight }); sendRateBox.Items.Add("Standard (5 ms PCM, 10/20 ms Opus)");
sendRateBox.Items.Add("Small (2.5 ms PCM, 5/10 ms Opus, LAN only)");
tightLatencyBox.Text = "Lock to au&dio clock, WASAPI sender";
tightLatencyBox.AccessibleName = "Lock to audio clock (Alt+D) — sender uses the WASAPI capture event for timing instead of a Stopwatch tick. Tightens delay; brief clicks possible if the link can't keep up.";
AddControlRow(panel, 0, "Audio &codec (Alt+C)", codecBox); FormLayoutRows.AddRow(panel, 0, "Audio &codec (Alt+C)", codecBox, c => c.Focus());
AddControlRow(panel, 1, "Send rat&e (Alt+E)", sendRateBox); FormLayoutRows.AddRow(panel, 1, "&Packet size (Alt+P)", sendRateBox, c => c.Focus());
var tightWrap = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill }; var tightWrap = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill };
tightWrap.Controls.Add(tightLatencyBox); tightWrap.Controls.Add(tightLatencyBox);
panel.Controls.Add(tightWrap, 1, 2); panel.Controls.Add(tightWrap, 1, 2);
@@ -143,37 +176,6 @@ internal sealed class ServiceProfileDialog : Form
tabs.TabPages.Add(page); tabs.TabPages.Add(page);
} }
private void BuildConnectivityTab()
{
var page = new TabPage("Connectivity");
var panel = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2, AutoScroll = true, Padding = new Padding(10) };
panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
AddListRow(panel, 0, "Peers to send to (Alt+&P)", peersList);
var addRow = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill };
addRow.Controls.Add(addPeerBox);
addRow.Controls.Add(addPeerButton);
addRow.Controls.Add(removePeerButton);
var addLabel = new MnemonicLabel { Text = "Add peer (Alt+&A)", AutoSize = true, Anchor = AnchorStyles.Left, MnemonicTarget = addPeerBox };
panel.Controls.Add(addLabel, 0, 1);
panel.Controls.Add(addRow, 1, 1);
addPeerButton.Click += (_, _) => AddPeerFromBox();
removePeerButton.Click += (_, _) => { if (peersList.SelectedItem is string s) { peersList.Items.Remove(s); } };
addPeerBox.KeyDown += (_, e) => { if (e.KeyCode == Keys.Enter) { AddPeerFromBox(); e.SuppressKeyPress = true; } };
var pwWrap = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill };
pwWrap.Controls.Add(passwordButton);
pwWrap.Controls.Add(passwordStatus);
panel.Controls.Add(pwWrap, 1, 2);
passwordButton.Click += (_, _) => SetPassword();
page.Controls.Add(panel);
tabs.TabPages.Add(page);
}
private void BuildButtonRow() private void BuildButtonRow()
{ {
var outer = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 2 }; var outer = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 2 };
@@ -200,14 +202,6 @@ internal sealed class ServiceProfileDialog : Form
return l; return l;
} }
private void AddControlRow(TableLayoutPanel panel, int row, string label, Control control)
{
var l = new MnemonicLabel { Text = label, AutoSize = true, Anchor = AnchorStyles.Left, MnemonicTarget = control };
l.Click += (_, _) => control.Focus();
panel.Controls.Add(l, 0, row);
panel.Controls.Add(control, 1, row);
}
// ---------------- data in/out ---------------- // ---------------- data in/out ----------------
private void LoadFromProfile() private void LoadFromProfile()
@@ -225,7 +219,7 @@ internal sealed class ServiceProfileDialog : Form
PopulateAppsList(); PopulateAppsList();
SelectCodec(); SelectCodec();
sendRateBox.SelectedItem = working.SendRate; sendRateBox.SelectedIndex = Math.Clamp((int)working.SendRate, 0, sendRateBox.Items.Count - 1);
tightLatencyBox.Checked = working.TightLatencyMode; tightLatencyBox.Checked = working.TightLatencyMode;
peersList.Items.Clear(); peersList.Items.Clear();
@@ -244,7 +238,7 @@ internal sealed class ServiceProfileDialog : Form
working.SelectedSendApplications = appsList.CheckedItems.OfType<AppRow>().Select(a => a.ProcessName).Distinct().ToList(); working.SelectedSendApplications = appsList.CheckedItems.OfType<AppRow>().Select(a => a.ProcessName).Distinct().ToList();
if (codecBox.SelectedItem is CodecChoice c) { working.Codec = c.Codec; working.OpusFrameSamplesPerChannel = c.OpusFrameSamples; } if (codecBox.SelectedItem is CodecChoice c) { working.Codec = c.Codec; working.OpusFrameSamplesPerChannel = c.OpusFrameSamples; }
if (sendRateBox.SelectedItem is SendRate r) working.SendRate = r; working.SendRate = (SendRate)Math.Max(0, sendRateBox.SelectedIndex);
working.TightLatencyMode = tightLatencyBox.Checked; working.TightLatencyMode = tightLatencyBox.Checked;
var peers = peersList.Items.OfType<string>().Distinct().ToList(); var peers = peersList.Items.OfType<string>().Distinct().ToList();
@@ -252,7 +246,7 @@ internal sealed class ServiceProfileDialog : Form
working.RememberedPeers = peers; working.RememberedPeers = peers;
} }
private void PopulateDeviceList(CheckedListBox list, IReadOnlyList<AudioDeviceChoice> devices, IReadOnlyList<string> checkedIds) private static void PopulateDeviceList(CheckedListBox list, IReadOnlyList<AudioDeviceChoice> devices, IReadOnlyList<string> checkedIds)
{ {
list.Items.Clear(); list.Items.Clear();
var wanted = new HashSet<string>(checkedIds, StringComparer.OrdinalIgnoreCase); var wanted = new HashSet<string>(checkedIds, StringComparer.OrdinalIgnoreCase);
@@ -326,7 +320,7 @@ internal sealed class ServiceProfileDialog : Form
{ {
var current = string.IsNullOrEmpty(working.Password) ? "" : RemSoundCrypto.Deobfuscate(working.Password); var current = string.IsNullOrEmpty(working.Password) ? "" : RemSoundCrypto.Deobfuscate(working.Password);
var result = ProfilePasswordDialog.Show(ServiceControl.ServiceProfileTitle, current); var result = ProfilePasswordDialog.Show(ServiceControl.ServiceProfileTitle, current);
if (result is null) return; // cancelled if (result is null) return;
working.Password = string.IsNullOrEmpty(result) ? null : RemSoundCrypto.Obfuscate(result); working.Password = string.IsNullOrEmpty(result) ? null : RemSoundCrypto.Obfuscate(result);
UpdatePasswordStatus(); UpdatePasswordStatus();
} }
@@ -368,8 +362,6 @@ internal sealed class ServiceProfileDialog : Form
private static Profile CloneProfile(Profile p) => private static Profile CloneProfile(Profile p) =>
System.Text.Json.JsonSerializer.Deserialize<Profile>(System.Text.Json.JsonSerializer.Serialize(p)) ?? new Profile(); System.Text.Json.JsonSerializer.Deserialize<Profile>(System.Text.Json.JsonSerializer.Serialize(p)) ?? new Profile();
/// <summary>One row in the applications list. Identity is the process name; display adds a
/// "(not running)" hint for a remembered-but-closed app.</summary>
private sealed class AppRow private sealed class AppRow
{ {
public string ProcessName { get; } public string ProcessName { get; }