Peer rename + details box on Connectivity tab; Add band dialog fixes

Add band dialog (from testing feedback):
 * Tab order is now Start, End, Gain, OK, Cancel (OK was after Cancel).
 * Gain box takes decimals like 1.5 (half-dB steps); parametric list shows one decimal.
 * The two frequency boxes start empty — nothing prepopulated to mislead; both required on OK.

New Connectivity-tab feature (held for next release):
 * Rename peer (Alt+M) opens a dialog to give a peer a friendly name, with a Clear custom name
   button. Names are keyed by the peer's MACHINE NAME (stable across restarts, IP changes and
   networks), stored machine-wide in AppConfig.PeerFriendlyNames, and resolved everywhere a peer
   shows: both peer lists (via PeerListItem.DisplayNameProvider), the volume/pan/EQ list, the
   status line and split-recording filenames. Manual-by-IP peers with no announced name fall back
   to keying by address.
 * Peer details (Alt+E): a read-only box for the highlighted connected peer showing name, machine
   name, IP, connected-for, link health + ping, what they're sending, and whether they're
   receiving our audio. "Sending: 2 devices on ASIO at 48 kHz, Opus" is derived from the live
   receive streams — each stream is one device and its lane gives WASAPI vs ASIO — so NO protocol
   change and no new privacy exposure. AudioReceiver.ActiveFormatsFromAddress added for this.

Manual updated (readme.html + MANUAL.md). Build clean; --selftest passes; deployed to both test
folders. Held for next release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-07 23:56:42 +01:00
co-authored by Claude Opus 4.8
parent 033edd776f
commit f35f6c3c28
8 changed files with 402 additions and 52 deletions
+81 -39
View File
@@ -1,22 +1,25 @@
using System.Globalization;
using RemSound.Core;
namespace RemSound.App;
/// <summary>Modal dialog for adding one parametric EQ band. The user sets a start frequency, an end
/// frequency and a gain; all three are spin-or-type boxes that refuse non-numbers and clamp to range.
/// While the dialog is open it previews the in-progress band live (so the peer's sound changes as the
/// values move); OK keeps the band, Cancel/Escape drops it and reverts the preview.</summary>
/// The two frequency boxes start EMPTY (no prepopulated value to mislead the user); the gain defaults
/// to a gentle boost. While the dialog is open it previews the in-progress band live (so the peer's
/// sound changes as the values move); OK keeps the band, Cancel/Escape drops it and reverts.</summary>
internal sealed class AddBandDialog : Form
{
private readonly NumericUpDown startFreq = new()
{
Minimum = (decimal)PeerEqBands.ParametricMinHz,
Maximum = (decimal)PeerEqBands.ParametricMaxHz,
Value = 200,
Value = (decimal)PeerEqBands.ParametricMinHz,
Increment = 10,
DecimalPlaces = 0,
Width = 120,
TextAlign = HorizontalAlignment.Right,
TabIndex = 0,
AccessibleName = "Start frequency in Hertz (Alt+S)",
};
@@ -24,11 +27,12 @@ internal sealed class AddBandDialog : Form
{
Minimum = (decimal)PeerEqBands.ParametricMinHz,
Maximum = (decimal)PeerEqBands.ParametricMaxHz,
Value = 2000,
Value = (decimal)PeerEqBands.ParametricMinHz,
Increment = 10,
DecimalPlaces = 0,
Width = 120,
TextAlign = HorizontalAlignment.Right,
TabIndex = 1,
AccessibleName = "End frequency in Hertz (Alt+E)",
};
@@ -37,27 +41,24 @@ internal sealed class AddBandDialog : Form
Minimum = -(decimal)PeerEqBands.MaxGainDb,
Maximum = (decimal)PeerEqBands.MaxGainDb,
Value = 3,
Increment = 1,
DecimalPlaces = 0,
Increment = 0.5m, // allow half-dB steps; typing any value like 1.5 works too
DecimalPlaces = 1,
Width = 120,
TextAlign = HorizontalAlignment.Right,
TabIndex = 2,
AccessibleName = "Gain in dB (Alt+G)",
};
private readonly Action<ParametricBand?>? livePreview;
private ParametricBand? resultBand;
private bool accepted;
/// <summary>The band the user built, valid only when <see cref="Form.ShowDialog()"/> returned OK.</summary>
public ParametricBand Result => new()
{
StartHz = (float)startFreq.Value,
EndHz = (float)endFreq.Value,
GainDb = (float)gainDb.Value,
};
public ParametricBand Result => resultBand ?? new ParametricBand();
/// <param name="livePreview">Called with the in-progress band on every value change so the caller
/// can apply it to the peer in real time, and with null when the dialog is cancelled/closed so the
/// caller reverts to the saved shaping.</param>
/// can apply it to the peer in real time, and with null when the band is incomplete or the dialog
/// is cancelled/closed so the caller reverts to the saved shaping.</param>
public AddBandDialog(Action<ParametricBand?>? livePreview = null)
{
this.livePreview = livePreview;
@@ -84,29 +85,39 @@ internal sealed class AddBandDialog : Form
AddRow(grid, 1, "&End frequency in Hz (Alt+E)", endFreq);
AddRow(grid, 2, "&Gain in dB (Alt+G)", gainDb);
var okButton = new Button { Text = "OK", AutoSize = true, DialogResult = DialogResult.None };
var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel };
var okButton = new Button { Text = "OK", AutoSize = true, DialogResult = DialogResult.None, TabIndex = 0 };
var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 1 };
okButton.Click += (_, _) => TryAccept();
// OK before Cancel, in both tab order and left-to-right layout.
var buttonRow = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.RightToLeft,
FlowDirection = FlowDirection.LeftToRight,
AutoSize = true,
TabIndex = 3,
};
buttonRow.Controls.Add(cancelButton);
buttonRow.Controls.Add(okButton);
buttonRow.Controls.Add(cancelButton);
grid.Controls.Add(buttonRow, 1, 3);
Controls.Add(grid);
AcceptButton = okButton;
CancelButton = cancelButton;
startFreq.ValueChanged += (_, _) => Preview();
endFreq.ValueChanged += (_, _) => Preview();
gainDb.ValueChanged += (_, _) => Preview();
foreach (var box in new[] { startFreq, endFreq, gainDb })
{
box.ValueChanged += (_, _) => Preview();
box.TextChanged += (_, _) => Preview();
}
Shown += (_, _) => { startFreq.Focus(); Preview(); };
Shown += (_, _) =>
{
// Blank the two frequency boxes so nothing is prepopulated; the user must enter both.
startFreq.Text = "";
endFreq.Text = "";
startFreq.Focus();
};
}
// The mnemonic hint is embedded in each NumericUpDown's AccessibleName; the label carries the
@@ -114,41 +125,72 @@ internal sealed class AddBandDialog : Form
private static void AddRow(TableLayoutPanel grid, int row, string labelText, NumericUpDown box)
{
var label = new Label { Text = labelText, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(0, 6, 8, 0) };
// A plain Label with '&' wires the mnemonic to the next control in tab order (the box).
grid.Controls.Add(label, 0, row);
grid.Controls.Add(box, 1, row);
}
private ParametricBand Current() => new()
private static bool TryReadHz(NumericUpDown box, out float hz)
{
StartHz = (float)startFreq.Value,
EndHz = (float)endFreq.Value,
GainDb = (float)gainDb.Value,
};
hz = 0f;
var t = box.Text.Trim();
if (t.Length == 0) return false;
if (!float.TryParse(t, NumberStyles.Float, CultureInfo.CurrentCulture, out hz)) return false;
return hz >= PeerEqBands.ParametricMinHz && hz <= PeerEqBands.ParametricMaxHz;
}
private void Preview() => livePreview?.Invoke(Current());
private bool TryReadDb(out float db)
{
db = 0f;
var t = gainDb.Text.Trim();
if (t.Length == 0) return false;
if (!float.TryParse(t, NumberStyles.Float, CultureInfo.CurrentCulture, out db)) return false;
db = Math.Clamp(db, -PeerEqBands.MaxGainDb, PeerEqBands.MaxGainDb);
return true;
}
private void Preview()
{
if (TryReadHz(startFreq, out float s) && TryReadHz(endFreq, out float e) && e > s)
livePreview?.Invoke(new ParametricBand { StartHz = s, EndHz = e, GainDb = TryReadDb(out float d) ? d : 0f });
else
livePreview?.Invoke(null); // incomplete / invalid → nothing to preview
}
private void TryAccept()
{
if (endFreq.Value <= startFreq.Value)
if (!TryReadHz(startFreq, out float s) || !TryReadHz(endFreq, out float e))
{
var page = new TaskDialogPage
{
Caption = "Add EQ band",
Heading = "End frequency must be higher than start",
Text = $"The end frequency ({endFreq.Value:0} Hz) must be higher than the start frequency ({startFreq.Value:0} Hz). Adjust one of them and try again.",
Icon = TaskDialogIcon.Warning,
Buttons = { TaskDialogButton.OK },
};
TaskDialog.ShowDialog(this, page);
Warn("Enter both frequencies",
$"Enter a start and an end frequency, each between {PeerEqBands.ParametricMinHz:0} and {PeerEqBands.ParametricMaxHz:0} Hz.");
startFreq.Focus();
return;
}
if (e <= s)
{
Warn("End frequency must be higher than start",
$"The end frequency ({e:0} Hz) must be higher than the start frequency ({s:0} Hz). Adjust one of them and try again.");
endFreq.Focus();
return;
}
resultBand = new ParametricBand { StartHz = s, EndHz = e, GainDb = TryReadDb(out float d) ? d : 0f };
accepted = true;
DialogResult = DialogResult.OK;
Close();
}
private void Warn(string heading, string text)
{
var page = new TaskDialogPage
{
Caption = "Add EQ band",
Heading = heading,
Text = text,
Icon = TaskDialogIcon.Warning,
Buttons = { TaskDialogButton.OK },
};
TaskDialog.ShowDialog(this, page);
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
base.OnFormClosing(e);
+179 -12
View File
@@ -268,6 +268,9 @@ public sealed class MainForm : Form
private readonly CheckedListBox rememberedPeersList = new() { CheckOnClick = true, Width = 430, Height = 90, AccessibleName = "Remembered peers (Alt+R)" };
private readonly Label rememberedPeersStatus = new() { AutoSize = true, Text = "No remembered peer selected." };
private readonly Button manualAddButton = new() { Text = "Add peer by IP (Alt+&A)", AutoSize = true, AccessibleName = "Add peer by IP" };
// Read-only details for the peer highlighted in the connected list, and a button to give it a name.
private readonly TextBox peerDetailsBox = new() { Multiline = true, ReadOnly = true, Width = 430, Height = 132, ScrollBars = ScrollBars.Vertical, AccessibleName = "Peer details (Alt+E)" };
private readonly Button renamePeerButton = new() { Text = "Rena&me peer (Alt+M)", AutoSize = true, AccessibleName = "Rename peer" };
// Per-profile "pin to exact addresses" toggle (#17). When ticked, RefreshKnownPeers skips the
// discovered-peer merge and the address-follow, so the profile's peers stay exactly as the user set.
private readonly AccessibleCheckBox lockPeerAddressesBox = new()
@@ -419,6 +422,12 @@ public sealed class MainForm : Form
// Display labels for selected peers so we can render them in the dialog list even when
// discovery has temporarily lost sight of them ("Foo (192.168.1.5) — offline").
private readonly Dictionary<Guid, string> selectedPeerLabels = [];
// Custom friendly names, keyed by peer identity (machine name, else address). Loaded from AppConfig
// (machine-wide) at startup and mirrored back on rename. Resolved to display names everywhere a peer
// shows — connected/discovered lists, the volume/pan/EQ list, the status line, split recordings.
private Dictionary<string, string> peerFriendlyNames = new(StringComparer.OrdinalIgnoreCase);
// When each connected peer (by per-run InstanceId) first went healthy — for the "connected for" line.
private readonly Dictionary<Guid, DateTime> peerConnectedSinceUtc = [];
// Anti-thrash state for the discovery-driven endpoint follow (see the peer-rebuild loop). A peer
// reachable at two addresses at once (a VPN address AND a LAN address, say) announces from both,
@@ -842,6 +851,11 @@ public sealed class MainForm : Form
.Select(kv => (kv.Value.Address, selectedPeerLabels.GetValueOrDefault(kv.Key, kv.Value.Address.ToString())))
.ToList();
// Load the machine-wide friendly-name book and make every peer list resolve display names
// through it. Set before any list is built so names show from the first render.
peerFriendlyNames = new Dictionary<string, string>(AppConfig.Load().PeerFriendlyNames ?? new(), StringComparer.OrdinalIgnoreCase);
PeerListItem.DisplayNameProvider = ResolvePeerDisplayName;
// --- Set accessibility names ---
// For these four controls the keyboard shortcut is included explicitly in both the
// visible label (set in BuildLayout) and the AccessibleName, instead of relying on the
@@ -2974,13 +2988,24 @@ public sealed class MainForm : Form
// Preferences dialog (File → Preferences, Ctrl+P) as the last two items.
// === Layout ===
// 6 rows: 02 the three peer lists, 3 manual-add, 4 the lock-to-fixed-addresses toggle, 5 status.
panel.RowCount = 6;
// 8 rows: 0 connected peers, 1 the details box + 2 rename button (both about the highlighted
// connected peer), 34 discovered & remembered lists, 5 manual-add, 6 the lock toggle, 7 status.
panel.RowCount = 8;
FormLayoutRows.AddCheckedListRow(panel, 0, "Connected peers (Alt+&C)", connectedPeersList, connectedPeersStatus, FocusListControl);
FormLayoutRows.AddCheckedListRow(panel, 1, "Discovered peers (Alt+&D)", discoveredPeersList, discoveredPeersStatus, FocusListControl);
FormLayoutRows.AddCheckedListRow(panel, 2, "Remembered peers (Alt+&R)", rememberedPeersList, rememberedPeersStatus, FocusListControl);
panel.Controls.Add(new Label { Text = "Manual peer", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 3);
panel.Controls.Add(manualAddButton, 1, 3);
// Details of, and a rename for, the peer highlighted in the connected list above.
var detailsLabel = new MnemonicLabel { Text = "Peer d&etails (Alt+E)", AutoSize = true, Anchor = AnchorStyles.Left, MnemonicTarget = peerDetailsBox };
detailsLabel.Click += (_, _) => peerDetailsBox.Focus();
panel.Controls.Add(detailsLabel, 0, 1);
panel.Controls.Add(peerDetailsBox, 1, 1);
renamePeerButton.Click += (_, _) => OnRenamePeer();
panel.Controls.Add(renamePeerButton, 1, 2);
connectedPeersList.SelectedIndexChanged += (_, _) => UpdatePeerDetails();
FormLayoutRows.AddCheckedListRow(panel, 3, "Discovered peers (Alt+&D)", discoveredPeersList, discoveredPeersStatus, FocusListControl);
FormLayoutRows.AddCheckedListRow(panel, 4, "Remembered peers (Alt+&R)", rememberedPeersList, rememberedPeersStatus, FocusListControl);
panel.Controls.Add(new Label { Text = "Manual peer", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 5);
panel.Controls.Add(manualAddButton, 1, 5);
lockPeerAddressesBox.Checked = settings.LoadLockPeerAddresses();
lockPeerAddressesBox.CheckedChanged += (_, _) =>
@@ -2989,14 +3014,15 @@ public sealed class MainForm : Form
MarkProfileDirty();
logFile.Event($"lock peer addresses: {(lockPeerAddressesBox.Checked ? "on" : "off")}");
};
panel.Controls.Add(lockPeerAddressesBox, 0, 4);
panel.Controls.Add(lockPeerAddressesBox, 0, 6);
panel.SetColumnSpan(lockPeerAddressesBox, 2);
// Connection status readout — last row, tab-into-able.
var statusLabel = new MnemonicLabel { Text = "Connection status (Alt+&S)", AutoSize = true, Anchor = AnchorStyles.Left, MnemonicTarget = statusReadout };
statusLabel.Click += (_, _) => statusReadout.Focus();
panel.Controls.Add(statusLabel, 0, 5);
panel.Controls.Add(statusReadout, 1, 5);
panel.Controls.Add(statusLabel, 0, 7);
panel.Controls.Add(statusReadout, 1, 7);
UpdatePeerDetails();
// Initial render so the box has content the moment the user tabs into it.
RefreshStatusReadout();
@@ -3495,12 +3521,19 @@ public sealed class MainForm : Form
// dB spoken as words — most NVDA users run with punctuation off and would never hear a "+" sign,
// so a boost must say "plus". ("minus" comes through on its own, but we spell both for symmetry.)
// The graphic sliders read whole dB; the parametric list keeps up to one decimal (e.g. 1.5 dB).
private static string FormatGainDb(float db)
{
if (MathF.Abs(db) < 0.5f) return "flat";
return db > 0 ? $"plus {db:0} dB" : $"minus {MathF.Abs(db):0} dB";
}
private static string FormatGainDbPrecise(float db)
{
if (MathF.Abs(db) < 0.05f) return "flat";
return db > 0 ? $"plus {db:0.#} dB" : $"minus {MathF.Abs(db):0.#} dB";
}
/// <summary>Whether a given peer is shaped right now: the profile-wide master switch AND that peer's
/// own tick (per-peer bypass) both have to be on.</summary>
private bool ShapingActiveFor(string? key)
@@ -3649,7 +3682,7 @@ public sealed class MainForm : Form
private sealed class ParametricBandItem(ParametricBand band)
{
public ParametricBand Band { get; } = band;
public override string ToString() => $"{Band.StartHz:0} Hz to {Band.EndHz:0} Hz, {FormatGainDb(Band.GainDb)}";
public override string ToString() => $"{Band.StartHz:0} Hz to {Band.EndHz:0} Hz, {FormatGainDbPrecise(Band.GainDb)}";
}
/// <summary>Send-side controls: codec + packet size on row 0, lock-to-audio-clock on
@@ -4189,11 +4222,145 @@ public sealed class MainForm : Form
? RoundToFive(rtt)
: null;
// Keep the friendly-name label fresh for the pan/EQ list, status line and recordings, and
// track when this peer first went healthy for the "connected for" line.
selectedPeerLabels[item.Peer.InstanceId] = ResolvePeerDisplayName(item.Peer);
if (isHealthy) { if (!peerConnectedSinceUtc.ContainsKey(item.Peer.InstanceId)) peerConnectedSinceUtc[item.Peer.InstanceId] = DateTime.UtcNow; }
else peerConnectedSinceUtc.Remove(item.Peer.InstanceId);
if (item.ToString() != prevText)
{
connectedPeersList.RefreshItemPublic(i);
}
}
UpdatePeerDetails();
}
// === Peer identity, friendly names, and the details box ===
/// <summary>The stable key a friendly name is stored under: the peer's machine name when it's a real
/// name, otherwise its address (the manual-by-IP case, where Name equals the address string).</summary>
private static string PeerIdentityKey(PeerAnnouncement peer)
{
var addr = peer.Address.ToString();
return string.IsNullOrWhiteSpace(peer.Name) || peer.Name == addr ? addr : peer.Name;
}
/// <summary>The name to show for a peer: its custom friendly name if set, else its machine name
/// (which for a manual-by-IP peer is the address). Used by every peer list via
/// <see cref="PeerListItem.DisplayNameProvider"/>.</summary>
private string ResolvePeerDisplayName(PeerAnnouncement peer)
=> peerFriendlyNames.TryGetValue(PeerIdentityKey(peer), out var custom) && !string.IsNullOrWhiteSpace(custom)
? custom
: peer.Name;
private PeerListItem? SelectedConnectedPeer() => SafeSelectedItem(connectedPeersList) as PeerListItem;
/// <summary>Refreshes the read-only details box for whichever peer is highlighted in the connected
/// list. Runs on selection change and each status tick (so "connected for" and ping stay live).</summary>
private void UpdatePeerDetails()
{
var item = SelectedConnectedPeer();
renamePeerButton.Enabled = item is not null;
var text = item is null ? "Select a connected peer to see its details." : BuildPeerDetailsText(item);
if (peerDetailsBox.Text != text) peerDetailsBox.Text = text;
}
private string BuildPeerDetailsText(PeerListItem item)
{
var peer = item.Peer;
var lines = new List<string>();
var display = ResolvePeerDisplayName(peer);
var machine = string.IsNullOrWhiteSpace(peer.Name) || peer.Name == peer.Address.ToString() ? null : peer.Name;
if (!string.Equals(display, machine, StringComparison.Ordinal) && !string.Equals(display, peer.Address.ToString(), StringComparison.Ordinal))
lines.Add($"Name: {display}");
lines.Add($"Machine name: {machine ?? "(unknown added by address)"}");
lines.Add($"Address: {peer.Address}");
if (peerConnectedSinceUtc.TryGetValue(peer.InstanceId, out var since))
lines.Add($"Connected for: {DescribeDuration(DateTime.UtcNow - since)}");
if (item.Status.Connected)
lines.Add(item.Status.RttMs is { } rtt ? $"Link: healthy, ping {rtt} ms" : "Link: healthy");
else
lines.Add("Link: not connected");
lines.Add("Sending: " + DescribeSending(peer));
lines.Add("Receiving your audio: " + (item.Status.Sending ? "yes" : "no"));
return string.Join(Environment.NewLine, lines);
}
/// <summary>"2 devices on ASIO at 48 kHz, Opus" — built from the live receive streams (each stream is
/// one capture device; its lane tells us WASAPI vs ASIO). Falls back gracefully when we're not
/// receiving that peer.</summary>
private string DescribeSending(PeerAnnouncement peer)
{
var formats = receiver.ActiveFormatsFromAddress(peer.Address);
if (formats.Count == 0)
return peer.CanSend ? "yes — turn on Receive audio to see the details" : "no";
string deviceWord = formats.Count == 1 ? "1 device" : $"{formats.Count} devices";
var apis = formats.Select(f => f.Lane == RenderRoute.AsioLane ? "ASIO" : "WASAPI").Distinct().OrderBy(a => a).ToList();
string apiPart = apis.Count == 1 ? $" on {apis[0]}" : $" on {string.Join(" and ", apis)}";
var rates = formats.Select(f => f.SampleRate).Distinct().ToList();
string ratePart = rates.Count == 1 ? $" at {rates[0]} Hz" : "";
var codecs = formats.Select(f => ((AudioTransportCodec)f.Codec) == AudioTransportCodec.Opus ? "Opus" : "PCM").Distinct().ToList();
string codecPart = codecs.Count == 1 ? $", {codecs[0]}" : "";
return $"{deviceWord}{apiPart}{ratePart}{codecPart}";
}
private static string DescribeDuration(TimeSpan d)
{
if (d.TotalSeconds < 60) return $"{(int)d.TotalSeconds} seconds";
if (d.TotalMinutes < 60) return $"{(int)d.TotalMinutes} minute{((int)d.TotalMinutes == 1 ? "" : "s")}";
int hours = (int)d.TotalHours;
int mins = d.Minutes;
return mins == 0 ? $"{hours} hour{(hours == 1 ? "" : "s")}" : $"{hours} hour{(hours == 1 ? "" : "s")} {mins} minute{(mins == 1 ? "" : "s")}";
}
private void OnRenamePeer()
{
var item = SelectedConnectedPeer();
if (item is null) return;
var peer = item.Peer;
var key = PeerIdentityKey(peer);
var machineForDisplay = string.IsNullOrWhiteSpace(peer.Name) || peer.Name == peer.Address.ToString()
? peer.Address.ToString()
: peer.Name;
peerFriendlyNames.TryGetValue(key, out var current);
using var dlg = new RenamePeerDialog(machineForDisplay, current);
if (dlg.ShowDialog(this) != DialogResult.OK) return;
ApplyFriendlyName(key, dlg.FriendlyName);
}
/// <summary>Store (or clear) a peer's friendly name in the machine-wide book, persist it, and
/// refresh every place a peer name shows.</summary>
private void ApplyFriendlyName(string identityKey, string? name)
{
if (string.IsNullOrWhiteSpace(name)) peerFriendlyNames.Remove(identityKey);
else peerFriendlyNames[identityKey] = name.Trim();
try
{
var cfg = AppConfig.Load();
cfg.PeerFriendlyNames = new Dictionary<string, string>(peerFriendlyNames, StringComparer.OrdinalIgnoreCase);
cfg.Save();
}
catch (Exception ex) { logFile.Event($"peer rename: failed to save friendly name: {ex.Message}"); }
// Refresh everywhere: connected/discovered lists rebuild their labels, the pan/EQ list re-reads
// its peer names, and the details box updates.
lastPanEqPeerSignature = "";
SyncAllPeerLists();
RefreshPanEqPeerList();
UpdatePeerDetails();
}
private void SyncDiscoveredList()
@@ -5872,7 +6039,7 @@ public sealed class MainForm : Form
foreach (var (id, oldEndpoint) in selectedPeerEndpoints.ToList())
{
if (!knownPeers.TryGetValue(id, out var peer)) continue;
selectedPeerLabels[id] = peer.Name;
selectedPeerLabels[id] = ResolvePeerDisplayName(peer);
var newEndpoint = new IPEndPoint(peer.Address, peer.AudioPort);
// Same address, or the one we're on is still healthy: nothing to do — and reset the
@@ -5912,7 +6079,7 @@ public sealed class MainForm : Form
private void SelectPeer(PeerAnnouncement peer, bool fromProfileRestore)
{
selectedPeerEndpoints[peer.InstanceId] = new IPEndPoint(peer.Address, peer.AudioPort);
selectedPeerLabels[peer.InstanceId] = peer.Name;
selectedPeerLabels[peer.InstanceId] = ResolvePeerDisplayName(peer);
logFile.Event($"peer selected: {peer.Name} {peer.Address}:{peer.AudioPort}");
InvalidateAutoTuneHistory();
PushAllowedReceiveSenders();
+7 -1
View File
@@ -63,6 +63,11 @@ internal sealed class PeerLineStatus
internal sealed class PeerListItem
{
/// <summary>Optional resolver mapping a peer to the display name to show — e.g. a user-set friendly
/// name. Set once by MainForm. When null (or it returns the machine name) behaviour is unchanged,
/// so this covers the connected AND discovered lists in one place.</summary>
public static Func<PeerAnnouncement, string>? DisplayNameProvider;
public PeerAnnouncement Peer { get; }
public PeerLineStatus Status { get; } = new();
@@ -79,7 +84,8 @@ internal sealed class PeerListItem
// Base label: "hostname (ip)" for discovered peers, just "ip" for manual-by-IP entries
// (where hostname equals the IP address). Avoids "192.168.1.95 (192.168.1.95)" duplication.
var addr = Peer.Address.ToString();
var basePart = Peer.Name == addr ? addr : $"{Peer.Name} ({addr})";
var name = DisplayNameProvider?.Invoke(Peer) ?? Peer.Name;
var basePart = name == addr ? addr : $"{name} ({addr})";
if (!Status.Connected)
{
+84
View File
@@ -0,0 +1,84 @@
namespace RemSound.App;
/// <summary>Modal dialog for giving a peer a friendly name. Shows the machine name for reference, a
/// text box for the custom name, and a "Clear custom name" button that wipes it back to the machine
/// name. OK (or Clear) returns DialogResult.OK; the caller reads <see cref="FriendlyName"/> — null
/// meaning "no custom name" (cleared or left blank). Cancel/Escape makes no change.</summary>
internal sealed class RenamePeerDialog : Form
{
private readonly TextBox nameBox = new()
{
Width = 260,
TabIndex = 0,
AccessibleName = "Friendly name (Alt+N)",
};
/// <summary>The chosen name, valid only when ShowDialog returned OK. Null = no custom name.</summary>
public string? FriendlyName { get; private set; }
public RenamePeerDialog(string machineName, string? currentCustomName)
{
Text = "Rename peer";
StartPosition = FormStartPosition.CenterParent;
FormBorderStyle = FormBorderStyle.FixedDialog;
MinimizeBox = false;
MaximizeBox = false;
ShowInTaskbar = false;
ClientSize = new Size(380, 170);
nameBox.Text = currentCustomName ?? "";
var grid = new TableLayoutPanel
{
Dock = DockStyle.Fill,
Padding = new Padding(12),
ColumnCount = 2,
RowCount = 3,
};
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
var machineLabel = new Label
{
Text = $"Machine name: {machineName}",
AutoSize = true,
Anchor = AnchorStyles.Left,
Margin = new Padding(0, 0, 0, 8),
};
grid.Controls.Add(machineLabel, 0, 0);
grid.SetColumnSpan(machineLabel, 2);
// Plain label with '&' wires the mnemonic to the next control in tab order (the text box).
var nameLabel = new Label { Text = "Friendly &name (Alt+N)", AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(0, 4, 8, 0) };
grid.Controls.Add(nameLabel, 0, 1);
grid.Controls.Add(nameBox, 1, 1);
var clearButton = new Button { Text = "&Clear custom name (Alt+C)", AutoSize = true, AccessibleName = "Clear custom name", TabIndex = 1 };
var okButton = new Button { Text = "OK", AutoSize = true, DialogResult = DialogResult.OK, TabIndex = 2 };
var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 3 };
clearButton.Click += (_, _) =>
{
FriendlyName = null;
DialogResult = DialogResult.OK;
Close();
};
okButton.Click += (_, _) =>
{
var t = nameBox.Text.Trim();
FriendlyName = t.Length == 0 ? null : t; // blank box = clear, same as the Clear button
};
var buttonRow = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.LeftToRight, AutoSize = true, TabIndex = 2 };
buttonRow.Controls.Add(clearButton);
buttonRow.Controls.Add(okButton);
buttonRow.Controls.Add(cancelButton);
grid.Controls.Add(buttonRow, 1, 2);
Controls.Add(grid);
AcceptButton = okButton;
CancelButton = cancelButton;
Shown += (_, _) => { nameBox.Focus(); nameBox.SelectAll(); };
}
}