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:
co-authored by
Claude Opus 4.8
parent
033edd776f
commit
f35f6c3c28
@@ -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
@@ -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: 0–2 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), 3–4 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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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(); };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user