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:
co-authored by
Claude Opus 4.8
parent
90a1ab3257
commit
e35f2f318a
@@ -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 += (_, _) =>
|
||||
|
||||
@@ -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 /
|
||||
|
||||
@@ -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), 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);
|
||||
// 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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 { } }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user