From 004e01306b41196c65dbe0b81ca9430a5930f3ac Mon Sep 17 00:00:00 2001 From: Ednunp <29843396+Ednunp@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:30:37 +0100 Subject: [PATCH] Service dialog: Connectivity tab now mirrors the main window Local checkpoint - NOT for public release. Ed: the connectivity box was totally different from the main window's - I'd rolled a bespoke listbox + textbox + buttons instead of copying the real tab. Now built like the main window's Connectivity tab: - Theme.SectionHeader("Peers") header. - Peers list is a wired CheckedListBox ("Peers to send to", Alt+C) with the shared CheckedListAccessibility so it announces state like every other list; a ticked peer is one the service sends to, Delete removes it. - "Add peer by IP (Alt+A)" button uses the SAME ManualPeerPrompt the main window uses, not an inline textbox. - Set password button kept. Checked peers persist as SelectedConnectedPeers (the send-to set); every listed peer is kept as RememberedPeers. Gate 25/25. Co-Authored-By: Claude Opus 4.8 --- src/RemSound.App/ServiceProfileDialog.cs | 76 ++++++++++++++---------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/src/RemSound.App/ServiceProfileDialog.cs b/src/RemSound.App/ServiceProfileDialog.cs index d1e5c07..0a1c0c2 100644 --- a/src/RemSound.App/ServiceProfileDialog.cs +++ b/src/RemSound.App/ServiceProfileDialog.cs @@ -16,11 +16,10 @@ internal sealed class ServiceProfileDialog : Form private readonly Profile working; private readonly QuietTabControl tabs = new() { Dock = DockStyle.Fill }; - // --- Connectivity tab --- - private readonly ListBox peersList = new() { Width = 460, Height = 120, AccessibleName = "Peers to send to (Alt+1)" }; - 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 removePeerButton = new() { Text = "&Remove", AutoSize = true, AccessibleName = "Remove selected peer" }; + // --- Connectivity tab (mirrors the main window: a wired peer list + Add peer by IP) --- + private readonly CheckedListBox peersList = new() { CheckOnClick = true, Width = 460, Height = 120, AccessibleName = "Peers to send to (Alt+C)" }; + private readonly Label peersStatus = new() { AutoSize = true, Text = "No peers." }; + private readonly Button manualAddButton = new() { Text = "Add peer by IP (Alt+&A)", AutoSize = true, AccessibleName = "Add peer by IP" }; private readonly Button passwordButton = new() { Text = "Set pass&word...", AutoSize = true, AccessibleName = "Set the service profile password" }; private readonly Label passwordStatus = new() { AutoSize = true }; @@ -98,24 +97,41 @@ internal sealed class ServiceProfileDialog : Form var page = new TabPage("Connectivity"); var panel = NewPanel(); - AddListRow(panel, 0, "Peers to send to (Alt+&1)", peersList); + var header = Theme.SectionHeader("Peers"); + panel.Controls.Add(header, 0, 0); + panel.SetColumnSpan(header, 2); - 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); + // The peer list, wired for the screen reader exactly like the main window. A ticked peer is one + // the service sends to; untick to keep it in the list but not send. Delete removes it entirely. + FormLayoutRows.AddCheckedListRow(panel, 1, "Peers to send to (Alt+&C)", peersList, peersStatus, l => l.Focus()); + CheckedListAccessibility.Wire(peersList, peersStatus, "peer"); + peersList.KeyDown += (_, e) => + { + if (e.KeyCode == Keys.Delete && peersList.SelectedIndex >= 0) + { + peersList.Items.RemoveAt(peersList.SelectedIndex); + e.Handled = true; e.SuppressKeyPress = true; + } + }; - 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; } }; + // Add a peer by address — the same "Add peer by IP" prompt the main window uses. + panel.Controls.Add(new Label { Text = "Manual peer", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 2); + panel.Controls.Add(manualAddButton, 1, 2); + manualAddButton.Click += (_, _) => + { + var entry = ManualPeerPrompt.Show(this); + if (string.IsNullOrWhiteSpace(entry)) return; + if (!peersList.Items.OfType().Any(p => string.Equals(p, entry, StringComparison.OrdinalIgnoreCase))) + { + var idx = peersList.Items.Add(entry.Trim()); + peersList.SetItemChecked(idx, true); + } + }; var pwWrap = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill }; pwWrap.Controls.Add(passwordButton); pwWrap.Controls.Add(passwordStatus); - panel.Controls.Add(pwWrap, 1, 2); + panel.Controls.Add(pwWrap, 1, 3); passwordButton.Click += (_, _) => SetPassword(); page.Controls.Add(panel); @@ -223,8 +239,15 @@ internal sealed class ServiceProfileDialog : Form tightLatencyBox.Checked = working.TightLatencyMode; peersList.Items.Clear(); - foreach (var p in (working.SelectedConnectedPeers.Count > 0 ? working.SelectedConnectedPeers : working.RememberedPeers).Distinct()) - peersList.Items.Add(p); + var allPeers = working.RememberedPeers.Concat(working.SelectedConnectedPeers) + .Where(p => !string.IsNullOrWhiteSpace(p)).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + var selectedPeers = new HashSet(working.SelectedConnectedPeers, StringComparer.OrdinalIgnoreCase); + foreach (var p in allPeers) + { + var idx = peersList.Items.Add(p); + // Ticked = the service sends to it. If the profile carried no explicit selection, tick all. + if (selectedPeers.Count == 0 || selectedPeers.Contains(p)) peersList.SetItemChecked(idx, true); + } } finally { suppressAppEvents = false; } } @@ -241,9 +264,9 @@ internal sealed class ServiceProfileDialog : Form working.SendRate = (SendRate)Math.Max(0, sendRateBox.SelectedIndex); working.TightLatencyMode = tightLatencyBox.Checked; - var peers = peersList.Items.OfType().Distinct().ToList(); - working.SelectedConnectedPeers = peers; - working.RememberedPeers = peers; + // Ticked peers are the ones the service sends to; keep every listed peer as remembered. + working.SelectedConnectedPeers = peersList.CheckedItems.OfType().Distinct().ToList(); + working.RememberedPeers = peersList.Items.OfType().Distinct().ToList(); } private static void PopulateDeviceList(CheckedListBox list, IReadOnlyList devices, IReadOnlyList checkedIds) @@ -307,15 +330,6 @@ internal sealed class ServiceProfileDialog : Form if (c.Parent is not null) c.Parent.Visible = visible; } - private void AddPeerFromBox() - { - var text = addPeerBox.Text.Trim(); - if (text.Length == 0) return; - if (!peersList.Items.OfType().Any(p => string.Equals(p, text, StringComparison.OrdinalIgnoreCase))) - peersList.Items.Add(text); - addPeerBox.Clear(); - } - private void SetPassword() { var current = string.IsNullOrEmpty(working.Password) ? "" : RemSoundCrypto.Deobfuscate(working.Password);