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
+13 -5
View File
@@ -76,14 +76,18 @@ internal sealed class AddBandDialog : Form
Dock = DockStyle.Fill,
Padding = new Padding(12),
ColumnCount = 2,
RowCount = 4,
RowCount = 5,
};
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
AddRow(grid, 0, "&Start frequency in Hz (Alt+S)", startFreq);
AddRow(grid, 1, "&End frequency in Hz (Alt+E)", endFreq);
AddRow(grid, 2, "&Gain in dB (Alt+G)", gainDb);
var heading = Theme.Heading("Add EQ band");
grid.Controls.Add(heading, 0, 0);
grid.SetColumnSpan(heading, 2);
AddRow(grid, 1, "&Start frequency in Hz (Alt+S)", startFreq);
AddRow(grid, 2, "&End frequency in Hz (Alt+E)", endFreq);
AddRow(grid, 3, "&Gain in dB (Alt+G)", gainDb);
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 };
@@ -99,7 +103,7 @@ internal sealed class AddBandDialog : Form
};
buttonRow.Controls.Add(okButton);
buttonRow.Controls.Add(cancelButton);
grid.Controls.Add(buttonRow, 1, 3);
grid.Controls.Add(buttonRow, 1, 4);
Controls.Add(grid);
AcceptButton = okButton;
@@ -109,6 +113,10 @@ internal sealed class AddBandDialog : Form
{
box.ValueChanged += (_, _) => Preview();
box.TextChanged += (_, _) => Preview();
// Select the whole value when the box gains focus, so a typed number REPLACES what's there
// instead of being inserted next to it (which parsed as junk and reverted). Deferred so it
// runs after focus settles.
box.Enter += (_, _) => box.BeginInvoke(() => box.Select(0, box.Text.Length));
}
Shown += (_, _) =>
+13 -2
View File
@@ -68,8 +68,19 @@ internal sealed class EqCurveControl : Panel
pts[x] = new PointF(x, y);
}
using var curvePen = new Pen(dark ? Color.FromArgb(90, 200, 255) : Color.FromArgb(0, 120, 200), 2f);
if (w >= 2) g.DrawLines(curvePen, pts);
Color curveColor = dark ? Color.FromArgb(90, 200, 255) : Color.FromArgb(0, 120, 200);
if (w >= 2)
{
// Soft translucent fill between the curve and the 0 dB line, then the line on top.
var fill = new PointF[pts.Length + 2];
Array.Copy(pts, fill, pts.Length);
fill[^2] = new PointF(w - 1, midY);
fill[^1] = new PointF(0, midY);
using (var fillBrush = new SolidBrush(Color.FromArgb(38, curveColor)))
g.FillPolygon(fillBrush, fill);
using var curvePen = new Pen(curveColor, 2f);
g.DrawLines(curvePen, pts);
}
}
/// <summary>Approximate combined EQ response in dB at one frequency. Cosmetic only — analytic bell /
+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()
+1 -1
View File
@@ -83,7 +83,7 @@ internal sealed class MainFormTrayController : IDisposable
// user hid + re-showed the icon. By computing the right text once, just before
// we set Visible = true for the first time, the shell sees the live state from
// NIM_ADD onward.
trayIcon.Icon = SystemIcons.Application;
trayIcon.Icon = Theme.AppIcon ?? SystemIcons.Application;
trayIcon.Visible = false;
trayIcon.DoubleClick += (_, _) => Restore();
+30 -1
View File
@@ -278,6 +278,15 @@ internal sealed class PreferencesDialog : Form
AutoSize = true,
};
// Colour theme picker — "Match Windows (system)" / Light / Dark. Applied at startup via
// Application.SetColorMode, so a change takes effect on the next launch. Purely visual.
private readonly ComboBox themeBox = new()
{
DropDownStyle = ComboBoxStyle.DropDownList,
Width = 200,
AccessibleName = "Colour theme (Alt+T)",
};
private readonly Label upnpStatusLabel = new()
{
Text = "",
@@ -620,6 +629,26 @@ internal sealed class PreferencesDialog : Form
try { cfg.Save(); } catch { /* harmless — choice just won't survive a restart */ }
};
themeBox.Items.AddRange(["Match Windows (system)", "Light", "Dark"]);
themeBox.SelectedIndex = (cfgForLoad.ThemeMode ?? "system").Trim().ToLowerInvariant() switch
{
"light" => 1,
"dark" => 2,
_ => 0,
};
themeBox.SelectedIndexChanged += (_, _) =>
{
var cfg = AppConfig.Load();
cfg.ThemeMode = themeBox.SelectedIndex switch { 1 => "light", 2 => "dark", _ => "system" };
try { cfg.Save(); } catch { /* harmless — choice just won't survive a restart */ }
};
var themeLabel = new MnemonicLabel { Text = "Colour &theme (Alt+T)", AutoSize = true, Anchor = AnchorStyles.Left, MnemonicTarget = themeBox };
var themeHint = new Label { Text = "— takes effect next launch", AutoSize = true, ForeColor = SystemColors.GrayText, Anchor = AnchorStyles.Left, Padding = new Padding(8, 4, 0, 0) };
var themeRow = new FlowLayoutPanel { AutoSize = true, WrapContents = false, Margin = new Padding(0, 4, 0, 0) };
themeRow.Controls.Add(themeLabel);
themeRow.Controls.Add(themeBox);
themeRow.Controls.Add(themeHint);
// Live UPnP status — the RouterPortMapper raises StatusChanged from a thread-pool
// thread, so marshal back onto the UI thread before touching the label. Subscribe
// on show and unsubscribe on close to avoid leaking the handler past the dialog.
@@ -813,7 +842,7 @@ internal sealed class PreferencesDialog : Form
// is a field (declared above) so OnShown can focus it when the dialog opens. Logging is its
// own tab (2026-06-19); the two logging controls moved off the General tab to lead it.
tabs.TabPages.Add(MakeTab("General",
browseProfilesFolderButton, acceptRemoteVolumeBox, upnpEnabledBox, upnpStatusLabel, showPanEqTabBox));
themeRow, browseProfilesFolderButton, acceptRemoteVolumeBox, upnpEnabledBox, upnpStatusLabel, showPanEqTabBox));
tabs.TabPages.Add(MakeTab("Audio cues", cueGroup));
tabs.TabPages.Add(MakeTab("Startup behaviour",
startMinimisedBox, startWithUserBox, startWithProfileBox, startupListPanel));
+17
View File
@@ -85,6 +85,23 @@ internal static class Program
ApplicationConfiguration.Initialize();
// Follow the user's chosen colour theme — "system" (match Windows light/dark) by default. This
// is an experimental WinForms API; guarded so any failure just leaves the classic light theme
// rather than stopping RemSound launching. Colours only — no effect on the screen reader.
try
{
var mode = (AppConfig.Load().ThemeMode ?? "system").Trim().ToLowerInvariant();
#pragma warning disable WFO5001
Application.SetColorMode(mode switch
{
"light" => SystemColorMode.Classic,
"dark" => SystemColorMode.Dark,
_ => SystemColorMode.System,
});
#pragma warning restore WFO5001
}
catch { /* older runtime or API change — stay on the classic light theme */ }
// Consolidate every older layout (loose files, or the interim config\ folder) into the single
// "user settings and logs" folder before anything reads config/profiles/logs. Idempotent +
// best-effort; upgrades users from any older build. Shown to the user once if files moved.
+10 -4
View File
@@ -197,7 +197,7 @@ internal sealed class RecordingSettingsDialog : Form
sourceList.SelectedIndexChanged += (_, _) =>
{
if (sourceList.SelectedIndex < 0) return;
working.Source = (RecordingSource)sourceList.SelectedIndex;
working.Source = SourceOrder[sourceList.SelectedIndex];
};
formatList.SelectedIndexChanged += (_, _) =>
@@ -372,10 +372,11 @@ internal sealed class RecordingSettingsDialog : Form
{
sourceList.BeginUpdate();
sourceList.Items.Clear();
// Order MUST match RecordingSource enum values 0/1/2.
// Display order is decoupled from the enum via SourceOrder (both first as the default, then
// received, then sent). Add the labels in that same order.
sourceList.Items.Add("Record both sent and received audio");
sourceList.Items.Add("Record all received audio");
sourceList.Items.Add("Record all sent audio");
sourceList.Items.Add("Record both sent and received audio");
sourceList.EndUpdate();
}
@@ -489,9 +490,14 @@ internal sealed class RecordingSettingsDialog : Form
channelList.EndUpdate();
}
// Display order for the recording-source list (decoupled from the RecordingSource enum values): both
// at the top as the default, then received, then sent.
private static readonly RecordingSource[] SourceOrder =
[RecordingSource.Both, RecordingSource.ReceivedOnly, RecordingSource.SentOnly];
private void SelectFromSource(RecordingSource src)
{
var idx = (int)src;
var idx = Array.IndexOf(SourceOrder, src);
if (idx >= 0 && idx < sourceList.Items.Count) sourceList.SelectedIndex = idx;
}
+7
View File
@@ -10,6 +10,10 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ApplicationHighDpiMode>SystemAware</ApplicationHighDpiMode>
<!-- Modern Windows UI font app-wide (was the legacy default). Purely visual; screen-reader
behaviour is unaffected. Applied by ApplicationConfiguration.Initialize() to every form. -->
<ApplicationDefaultFont>Segoe UI, 9pt</ApplicationDefaultFont>
<ApplicationIcon>remsound.ico</ApplicationIcon>
<!-- Release version. The self-updater (RemSoundUpdater) compares this against the
tag_name on the latest GitHub release; bump it on every public release. The
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
@@ -18,6 +22,9 @@
</PropertyGroup>
<ItemGroup>
<!-- App icon (also set via ApplicationIcon for the exe). Embedded so the forms and the tray icon
can load the full multi-size icon at runtime for crisp title-bar / taskbar / tray rendering. -->
<EmbeddedResource Include="remsound.ico" />
<ProjectReference Include="..\RemSound.Core\RemSound.Core.csproj" />
<ProjectReference Include="..\RemSound.Sender\RemSound.Sender.csproj" />
<ProjectReference Include="..\RemSound.Receiver\RemSound.Receiver.csproj" />
+9 -5
View File
@@ -33,11 +33,15 @@ internal sealed class RenamePeerDialog : Form
Dock = DockStyle.Fill,
Padding = new Padding(12),
ColumnCount = 2,
RowCount = 3,
RowCount = 4,
};
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
var heading = Theme.Heading("Rename peer");
grid.Controls.Add(heading, 0, 0);
grid.SetColumnSpan(heading, 2);
var machineLabel = new Label
{
Text = $"Machine name: {machineName}",
@@ -45,13 +49,13 @@ internal sealed class RenamePeerDialog : Form
Anchor = AnchorStyles.Left,
Margin = new Padding(0, 0, 0, 8),
};
grid.Controls.Add(machineLabel, 0, 0);
grid.Controls.Add(machineLabel, 0, 1);
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);
grid.Controls.Add(nameLabel, 0, 2);
grid.Controls.Add(nameBox, 1, 2);
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 };
@@ -73,7 +77,7 @@ internal sealed class RenamePeerDialog : Form
buttonRow.Controls.Add(clearButton);
buttonRow.Controls.Add(okButton);
buttonRow.Controls.Add(cancelButton);
grid.Controls.Add(buttonRow, 1, 2);
grid.Controls.Add(buttonRow, 1, 3);
Controls.Add(grid);
AcceptButton = okButton;
+115
View File
@@ -0,0 +1,115 @@
using System.Drawing.Drawing2D;
namespace RemSound.App;
/// <summary>Central visual style for RemSound. The app font is set globally (csproj
/// ApplicationDefaultFont) and dark/light follows the OS (Application.SetColorMode in Program.Main), so
/// this only holds the extra touches: an accent colour, section-header + dialog-heading label factories,
/// and health-status colours. All of it is visual only — it changes no control types and no
/// accessibility wiring, so the screen-reader experience is untouched.</summary>
internal static class Theme
{
private static Icon? appIcon;
private static bool triedIcon;
/// <summary>The RemSound window icon, loaded once from the embedded multi-size .ico so title bars,
/// the taskbar, Alt+Tab and the tray icon all render crisply. Null if it can't be loaded.</summary>
public static Icon? AppIcon
{
get
{
if (!triedIcon)
{
triedIcon = true;
try
{
var asm = typeof(Theme).Assembly;
var name = Array.Find(asm.GetManifestResourceNames(),
n => n.EndsWith("remsound.ico", StringComparison.OrdinalIgnoreCase));
if (name is not null)
{
using var s = asm.GetManifestResourceStream(name);
if (s is not null) appIcon = new Icon(s);
}
}
catch { appIcon = null; }
}
return appIcon;
}
}
/// <summary>True when the app is currently showing on a dark background. Cosmetic heuristic
/// (mirrors <see cref="EqCurveControl"/>) — used to pick contrasting accent/health colours.</summary>
public static bool IsDark => SystemColors.Window.GetBrightness() < 0.5f;
/// <summary>Accent used for section headings and the EQ curve. Lighter blue in dark mode.</summary>
public static Color Accent => IsDark ? Color.FromArgb(88, 166, 255) : Color.FromArgb(0, 99, 177);
public static Color Healthy => IsDark ? Color.FromArgb(70, 200, 100) : Color.FromArgb(24, 140, 56);
public static Color Warning => IsDark ? Color.FromArgb(232, 184, 64) : Color.FromArgb(176, 120, 0);
public static Color Bad => IsDark ? Color.FromArgb(240, 100, 100) : Color.FromArgb(190, 40, 40);
public static Color Neutral => SystemColors.GrayText;
/// <summary>A bold, accent-coloured section header — groups a run of rows on the busier tabs. Not a
/// tab stop and carries no shortcut; it's static text the screen reader simply reads as a heading.</summary>
public static Label SectionHeader(string text) => new()
{
Text = text,
AutoSize = true,
Font = new Font("Segoe UI Semibold", 9.75f, FontStyle.Bold),
ForeColor = Accent,
Margin = new Padding(0, 10, 0, 2),
Anchor = AnchorStyles.Left,
};
/// <summary>A bold heading label for the top of a dialog.</summary>
public static Label Heading(string text) => new()
{
Text = text,
AutoSize = true,
Font = new Font("Segoe UI Semibold", 11f, FontStyle.Bold),
Margin = new Padding(0, 0, 0, 8),
Anchor = AnchorStyles.Left,
};
}
/// <summary>A small filled circle used as a colour cue (e.g. connection health) beside a text label. It
/// is deliberately invisible to the screen reader — not a tab stop, empty accessible name, Graphic role
/// — so the neighbouring text (which NVDA does read) remains the single source of truth.</summary>
internal sealed class StatusDot : Control
{
private Color dot = SystemColors.GrayText;
public StatusDot()
{
TabStop = false;
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer
| ControlStyles.UserPaint | ControlStyles.SupportsTransparentBackColor, true);
Size = new Size(12, 12);
Margin = new Padding(0, 5, 6, 0);
AccessibleName = "";
}
public void SetColor(Color c)
{
if (dot == c) return;
dot = c;
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
int d = Math.Min(Width, Height) - 2;
using var b = new SolidBrush(dot);
e.Graphics.FillEllipse(b, (Width - d) / 2f, (Height - d) / 2f, d, d);
}
protected override AccessibleObject CreateAccessibilityInstance() => new HiddenAcc(this);
private sealed class HiddenAcc(Control owner) : ControlAccessibleObject(owner)
{
public override AccessibleRole Role => AccessibleRole.Graphic;
public override string? Name { get => ""; set { } }
}
}