Testing-feedback fixes: tab order, label wording, parametric editing, recording default

* Connectivity tab order: Add peer by IP now sits right after Rename peer (before the Discovered
   list), grouped with the connected-peer actions, per Ed's requested order. Dropped the second
   section header (only the lock toggle was under it).
 * Audio I/O labels: "Set volume for all received audio" → "Master volume for received audio" (the
   code never got this rename, only the manual had). Device lists now say "audio": "...for received
   audio" and "WASAPI/ASIO audio inputs/outputs to send". Labels + AccessibleNames updated together.
 * Add EQ band dialog: the spin/edit boxes now select-all on focus, so a typed value REPLACES what's
   there instead of being inserted next to it and reverting (typing 2.5 over 4.0 now works).
 * Parametric bands list: Left/Right arrow nudge the selected band's gain by half a dB, live —
   up/down still move between bands. NVDA re-reads the band's new dB.
 * Recording settings: source list reordered to Both (top, now the default) / Received / Sent;
   display order decoupled from the RecordingSource enum. Default RecordingSettings.Source = Both.
 * Preferences: Colour theme is now first in the General tab order.

Manual updated. 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-08 11:36:02 +01:00
co-authored by Claude Opus 4.8
parent 90a1ab3257
commit e35f2f318a
14 changed files with 339 additions and 101 deletions
+68 -32
View File
@@ -211,6 +211,9 @@ public sealed class MainForm : Form
// Profile-management buttons retired 2026-05-08 — these actions live in File menu now.
// The methods (SaveProfileAs / UpdateExistingProfile) are still here; they're called from
// the menu item Click handlers in BuildFileMenu.
// A colour cue for connection health (green streaming / amber idle / grey disconnected) beside the
// health text. Purely visual — invisible to NVDA; the health text is unchanged.
private readonly StatusDot healthDot = new();
private readonly Label healthLabel = new() { Text = "Health: disconnected", AutoSize = true };
private readonly Label statusLabel = new() { Text = "Disconnected", AutoSize = true };
@@ -750,6 +753,7 @@ public sealed class MainForm : Form
Height = 600;
MinimumSize = new Size(560, 520);
StartPosition = FormStartPosition.CenterScreen;
if (Theme.AppIcon is { } appIcon) Icon = appIcon; // window title bar + Alt+Tab + taskbar
// No AccessibleName / AccessibleRole on the form. Andre's accessible app does not
// set these and NVDA reads cleanly there; setting them here was over-engineering.
@@ -881,16 +885,16 @@ public sealed class MainForm : Form
// AccessibleName text guarantees NVDA announces it consistently right after the
// control name. CheckBoxes own their own &-mnemonic via their Text and don't need
// the suffix in AccessibleName — they're left as bare names.
volumeBar.AccessibleName = "Set volume for all received audio (Alt+V)";
receiveOutputDevicesList.AccessibleName = "WASAPI outputs for received sound (Alt+3)";
volumeBar.AccessibleName = "Master volume for received audio (Alt+V)";
receiveOutputDevicesList.AccessibleName = "WASAPI outputs for received audio (Alt+3)";
receiveOutputDevicesStatusLabel.AccessibleName = "Selected receive output device status";
sendOutputDevicesList.AccessibleName = "WASAPI outputs to send (Alt+4)";
sendOutputDevicesList.AccessibleName = "WASAPI audio outputs to send (Alt+4)";
sendOutputDevicesStatusLabel.AccessibleName = "Selected output device status";
sendInputDevicesList.AccessibleName = "WASAPI inputs to send (Alt+5)";
sendInputDevicesList.AccessibleName = "WASAPI audio inputs to send (Alt+5)";
sendInputDevicesStatusLabel.AccessibleName = "Selected input device status";
asioReceiveOutputDevicesList.AccessibleName = "ASIO outputs for received sound (Alt+1)";
asioReceiveOutputDevicesList.AccessibleName = "ASIO outputs for received audio (Alt+1)";
asioReceiveOutputDevicesStatusLabel.AccessibleName = "Selected ASIO receive channel status";
asioSendDevicesList.AccessibleName = "ASIO inputs to send (Alt+2)";
asioSendDevicesList.AccessibleName = "ASIO audio inputs to send (Alt+2)";
asioSendDevicesStatusLabel.AccessibleName = "Selected ASIO send channel status";
// Keyboard shortcuts / Minimise to tray / Save / Save as buttons retired 2026-05-08
// (now File menu items in BuildFileMenu).
@@ -1769,6 +1773,7 @@ public sealed class MainForm : Form
WrapContents = false,
Padding = new Padding(8, 4, 8, 4),
};
statusPanel.Controls.Add(healthDot);
statusPanel.Controls.Add(healthLabel);
statusPanel.Controls.Add(new Label { Text = " ", AutoSize = true });
statusPanel.Controls.Add(statusLabel);
@@ -3013,24 +3018,32 @@ public sealed class MainForm : Form
// Preferences dialog (File → Preferences, Ctrl+P) as the last two items.
// === Layout ===
// 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);
// Tab order (row order = add order): 0 "Peers" header; 1 connected peers; 2 details box; 3 rename;
// 4 add-by-IP; 5 discovered; 6 remembered; 7 lock toggle; 8 status. The "Peers" header is a
// visual grouping only. Add-by-IP sits with the connected-peer actions, per Ed's requested order.
panel.RowCount = 9;
var peersHeader = Theme.SectionHeader("Peers");
panel.Controls.Add(peersHeader, 0, 0);
panel.SetColumnSpan(peersHeader, 2);
FormLayoutRows.AddCheckedListRow(panel, 1, "Connected peers (Alt+&C)", connectedPeersList, connectedPeersStatus, FocusListControl);
// 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);
panel.Controls.Add(detailsLabel, 0, 2);
panel.Controls.Add(peerDetailsBox, 1, 2);
renamePeerButton.Click += (_, _) => OnRenamePeer();
panel.Controls.Add(renamePeerButton, 1, 2);
panel.Controls.Add(renamePeerButton, 1, 3);
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);
// Add a peer by address — grouped with the connected-peer actions, before the discovery lists.
panel.Controls.Add(new Label { Text = "Manual peer", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 4);
panel.Controls.Add(manualAddButton, 1, 4);
FormLayoutRows.AddCheckedListRow(panel, 5, "Discovered peers (Alt+&D)", discoveredPeersList, discoveredPeersStatus, FocusListControl);
FormLayoutRows.AddCheckedListRow(panel, 6, "Remembered peers (Alt+&R)", rememberedPeersList, rememberedPeersStatus, FocusListControl);
lockPeerAddressesBox.Checked = settings.LoadLockPeerAddresses();
lockPeerAddressesBox.CheckedChanged += (_, _) =>
@@ -3039,14 +3052,14 @@ public sealed class MainForm : Form
MarkProfileDirty();
logFile.Event($"lock peer addresses: {(lockPeerAddressesBox.Checked ? "on" : "off")}");
};
panel.Controls.Add(lockPeerAddressesBox, 0, 6);
panel.Controls.Add(lockPeerAddressesBox, 0, 7);
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, 7);
panel.Controls.Add(statusReadout, 1, 7);
panel.Controls.Add(statusLabel, 0, 8);
panel.Controls.Add(statusReadout, 1, 8);
UpdatePeerDetails();
// Initial render so the box has content the moment the user tabs into it.
@@ -3112,15 +3125,15 @@ public sealed class MainForm : Form
var receiveCheckboxPanel = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill };
receiveCheckboxPanel.Controls.Add(receiveAudioCheckbox);
panel.Controls.Add(receiveCheckboxPanel, 1, 2);
receiveOutputDevicesLabel = FormLayoutRows.AddCheckedListRow(panel, 3, "WASAPI outputs for received sound (Alt+&3)", receiveOutputDevicesList, receiveOutputDevicesStatusLabel, FocusListControl);
asioReceiveOutputDevicesLabel = FormLayoutRows.AddCheckedListRow(panel, 4, "ASIO outputs for received sound (Alt+&1)", asioReceiveOutputDevicesList, asioReceiveOutputDevicesStatusLabel, FocusListControl);
FormLayoutRows.AddRow(panel, 5, "Set volume for all received audio (Alt+&V)", volumeBar, FocusControl);
receiveOutputDevicesLabel = FormLayoutRows.AddCheckedListRow(panel, 3, "WASAPI outputs for received audio (Alt+&3)", receiveOutputDevicesList, receiveOutputDevicesStatusLabel, FocusListControl);
asioReceiveOutputDevicesLabel = FormLayoutRows.AddCheckedListRow(panel, 4, "ASIO outputs for received audio (Alt+&1)", asioReceiveOutputDevicesList, asioReceiveOutputDevicesStatusLabel, FocusListControl);
FormLayoutRows.AddRow(panel, 5, "Master volume for received audio (Alt+&V)", volumeBar, FocusControl);
var sendCheckboxPanel = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill };
sendCheckboxPanel.Controls.Add(sendMyAudioCheckbox);
panel.Controls.Add(sendCheckboxPanel, 1, 6);
sendOutputDevicesLabel = FormLayoutRows.AddCheckedListRow(panel, 7, "WASAPI outputs to send (Alt+&4)", sendOutputDevicesList, sendOutputDevicesStatusLabel, FocusListControl);
sendInputDevicesLabel = FormLayoutRows.AddCheckedListRow(panel, 8, "WASAPI inputs to send (Alt+&5)", sendInputDevicesList, sendInputDevicesStatusLabel, FocusListControl);
asioSendDevicesLabel = FormLayoutRows.AddCheckedListRow(panel, 9, "ASIO inputs to send (Alt+&2)", asioSendDevicesList, asioSendDevicesStatusLabel, FocusListControl);
sendOutputDevicesLabel = FormLayoutRows.AddCheckedListRow(panel, 7, "WASAPI audio outputs to send (Alt+&4)", sendOutputDevicesList, sendOutputDevicesStatusLabel, FocusListControl);
sendInputDevicesLabel = FormLayoutRows.AddCheckedListRow(panel, 8, "WASAPI audio inputs to send (Alt+&5)", sendInputDevicesList, sendInputDevicesStatusLabel, FocusListControl);
asioSendDevicesLabel = FormLayoutRows.AddCheckedListRow(panel, 9, "ASIO audio inputs to send (Alt+&2)", asioSendDevicesList, asioSendDevicesStatusLabel, FocusListControl);
audioIOTabPage.Controls.Add(panel);
}
@@ -3697,6 +3710,24 @@ public sealed class MainForm : Form
OnDeleteParametricBands();
e.Handled = true;
e.SuppressKeyPress = true;
return;
}
// Left / Right nudge the selected band(s) gain by half a dB — quick on-the-fly editing, heard
// in real time. Up / Down still move between bands as normal.
if ((e.KeyCode == Keys.Left || e.KeyCode == Keys.Right)
&& selectedShapingKey is not null && parametricBandList.SelectedItems.Count > 0)
{
float delta = e.KeyCode == Keys.Right ? 0.5f : -0.5f;
foreach (var it in parametricBandList.SelectedItems.Cast<ParametricBandItem>())
it.Band.GainDb = Math.Clamp(it.Band.GainDb + delta, -PeerEqBands.MaxGainDb, PeerEqBands.MaxGainDb);
parametricBandList.Invalidate(); // redraw the rows with their updated dB (ToString reads live)
ApplyPeerShaping(selectedShapingKey);
UpdateEqCurve();
MarkProfileDirty();
// Make NVDA re-read the focused band with its new dB value.
WinEventNotifier.NotifyFocus(parametricBandList);
e.Handled = true;
e.SuppressKeyPress = true;
}
}
@@ -4430,13 +4461,16 @@ public sealed class MainForm : Form
ClientSize = new Size(600, 420),
};
var root = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(12), ColumnCount = 1, RowCount = 4 };
var root = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(12), ColumnCount = 1, RowCount = 5 };
root.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.Controls.Add(Theme.Heading("Named peers"), 0, 0);
var intro = new Label
{
Text = "The peers you've given a name to. Rename or delete any of them. Deleting forgets the "
@@ -4445,14 +4479,14 @@ public sealed class MainForm : Form
MaximumSize = new Size(560, 0),
Margin = new Padding(0, 0, 0, 8),
};
root.Controls.Add(intro, 0, 0);
root.Controls.Add(intro, 0, 1);
// Plain label with '&' focuses the next control in tab order (the list).
var peersLabel = new Label { Text = "&Peers", AutoSize = true, Anchor = AnchorStyles.Left };
root.Controls.Add(peersLabel, 0, 1);
root.Controls.Add(peersLabel, 0, 2);
var list = new ListBox { Dock = DockStyle.Fill, IntegralHeight = false, AccessibleName = "Peers (Alt+P)", TabIndex = 0 };
root.Controls.Add(list, 0, 2);
root.Controls.Add(list, 0, 3);
var renameBtn = new Button { Text = "&Rename (Alt+R)", AutoSize = true, AccessibleName = "Rename", TabIndex = 1 };
var deleteBtn = new Button { Text = "&Delete (Alt+D)", AutoSize = true, AccessibleName = "Delete", TabIndex = 2 };
@@ -4461,7 +4495,7 @@ public sealed class MainForm : Form
buttonRow.Controls.Add(renameBtn);
buttonRow.Controls.Add(deleteBtn);
buttonRow.Controls.Add(closeBtn);
root.Controls.Add(buttonRow, 0, 3);
root.Controls.Add(buttonRow, 0, 4);
dialog.Controls.Add(root);
dialog.AcceptButton = closeBtn;
@@ -6605,9 +6639,11 @@ public sealed class MainForm : Form
var peerCount = knownPeers.Count;
var hbSummary = heartbeatService?.GetHealthSummary() ?? "no peers";
statusLabel.Text = $"Connected for {since}. {peerCount} peer(s) known. {sendText}. {receiveText}. Heartbeat: {hbSummary}.";
bool streaming = connected && (sender.IsRunning || receiver.IsRunning);
healthLabel.Text = connected
? sender.IsRunning || receiver.IsRunning ? "Health: streaming" : "Health: idle"
? streaming ? "Health: streaming" : "Health: idle"
: "Health: disconnected";
healthDot.SetColor(!connected ? Theme.Neutral : streaming ? Theme.Healthy : Theme.Warning);
}
private void SnapshotLogIfDue()