New Preferences "Appearance" tab: theme + show-pan/EQ moved in, tab reordering, peer-list toggles

Adds an Appearance tab (after General) and moves the colour theme and "show volume/pan/EQ tab" into
it, per Ed. New on that tab:
 * Main tab order — a list of the four main-window tabs (all shown, even when the pan/EQ tab is
   hidden) with Move up / Move down buttons to reorder them. Saved to AppConfig.MainTabOrder and
   applied by the new ApplyMainTabLayout (rebuilds the tab strip in order, dropping pan/EQ when off,
   preserving selection). Replaces RefreshPanEqTabVisibility.
 * Enable discovered / remembered peers lists on the Connectivity tab (AppConfig.ShowDiscoveredPeers
   / ShowRememberedPeers, both default on). RefreshConnectivityListVisibility hides the row's label
   and its list wrapper when off. Row labels are now captured for this.

All apply when Preferences closes. 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 12:56:06 +01:00
co-authored by Claude Opus 4.8
parent 9c8e50667b
commit 78684f29f1
5 changed files with 182 additions and 26 deletions
+72 -22
View File
@@ -274,6 +274,10 @@ public sealed class MainForm : Form
// 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" };
// The Discovered / Remembered row labels, captured so those rows can be hidden when the user turns
// the lists off on the Preferences Appearance tab.
private MnemonicLabel? discoveredPeersLabel;
private MnemonicLabel? rememberedPeersLabel;
// 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()
@@ -1749,11 +1753,8 @@ public sealed class MainForm : Form
BuildAudioProfileTab();
BuildPanEqTab();
mainTabControl.TabPages.Add(connectivityTabPage);
mainTabControl.TabPages.Add(audioIOTabPage);
mainTabControl.TabPages.Add(audioProfileTabPage);
// Insert the optional Pan-and-EQ tab (just before Audio profile) if the preference is on.
RefreshPanEqTabVisibility();
// Add the tabs in the user's saved order (and honour the "show pan/EQ tab" toggle).
ApplyMainTabLayout();
// No SelectedIndexChanged handler. No focus management on tab change. Andre's
// accessible app does ZERO event hooking on TabControl — relies entirely on
// default WinForms + NVDA behaviour. Per Ed's repeated request: arrow keys cycle
@@ -2595,8 +2596,9 @@ public sealed class MainForm : Form
unsubscribeUpnpStatusChanged: handler => routerPortMapper.StatusChanged -= handler);
dialog.ShowDialog(this);
if (dialog.ChangedAnyProfileSetting) MarkProfileDirty();
// The "Show pan and EQ tab" preference may have been toggled — add/remove the tab to match.
RefreshPanEqTabVisibility();
// Appearance-tab changes (tab order, show pan/EQ tab, show discovered/remembered lists) apply now.
ApplyMainTabLayout();
RefreshConnectivityListVisibility();
// The Preferences dialog includes per-cue Browse buttons that can change custom
// WAV paths in AppConfig.CustomCuePaths. Reload the cached SoundPlayer instances
// here unconditionally — cheap, only six small files, and guarantees the next
@@ -3038,8 +3040,8 @@ public sealed class MainForm : Form
panel.Controls.Add(renamePeerButton, 1, 3);
connectedPeersList.SelectedIndexChanged += (_, _) => UpdatePeerDetails();
FormLayoutRows.AddCheckedListRow(panel, 4, "Discovered peers (Alt+&D)", discoveredPeersList, discoveredPeersStatus, FocusListControl);
FormLayoutRows.AddCheckedListRow(panel, 5, "Remembered peers (Alt+&R)", rememberedPeersList, rememberedPeersStatus, FocusListControl);
discoveredPeersLabel = FormLayoutRows.AddCheckedListRow(panel, 4, "Discovered peers (Alt+&D)", discoveredPeersList, discoveredPeersStatus, FocusListControl);
rememberedPeersLabel = FormLayoutRows.AddCheckedListRow(panel, 5, "Remembered peers (Alt+&R)", rememberedPeersList, rememberedPeersStatus, FocusListControl);
// Add a peer by address, then the lock toggle — the manual / advanced options after the lists.
panel.Controls.Add(new Label { Text = "Manual peer", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 6);
@@ -3083,6 +3085,23 @@ public sealed class MainForm : Form
connectivityTabPage.Controls.Add(panel);
// Initial population so screen readers see something on first open.
SyncAllPeerLists();
RefreshConnectivityListVisibility();
}
/// <summary>Shows or hides the Discovered / Remembered peer rows to match the Appearance-tab
/// preferences. Hiding both a row's label and its list wrapper collapses the row. Called at build
/// and after Preferences close.</summary>
private void RefreshConnectivityListVisibility()
{
var cfg = AppConfig.Load();
SetConnectivityRowVisible(discoveredPeersLabel, discoveredPeersList, cfg.ShowDiscoveredPeers);
SetConnectivityRowVisible(rememberedPeersLabel, rememberedPeersList, cfg.ShowRememberedPeers);
}
private static void SetConnectivityRowVisible(Control? label, Control list, bool visible)
{
if (label is not null) label.Visible = visible;
if (list.Parent is not null) list.Parent.Visible = visible; // the FlowLayoutPanel wrapper
}
/// <summary>Audio I/O tab — full content. All the existing main-form audio controls
@@ -3288,22 +3307,53 @@ public sealed class MainForm : Form
OnPanEqPeerSelected();
}
/// <summary>Adds (or removes) the Pan-and-EQ tab to match the machine-wide "Show pan and EQ tab"
/// preference. Placed just before the Audio profile tab. Called at build and after Preferences close.</summary>
private void RefreshPanEqTabVisibility()
// The main-window tabs' stable keys, in the default order. The user can reorder them on the
// Preferences Appearance tab; "paneq" is only shown when the "show pan/EQ tab" preference is on.
private static readonly string[] DefaultTabOrder = ["connectivity", "audioio", "paneq", "audioprofile"];
private TabPage? TabPageForKey(string key) => key switch
{
bool show = AppConfig.Load().ShowPanEqTab;
bool present = mainTabControl.TabPages.Contains(panEqTabPage);
if (show && !present)
"connectivity" => connectivityTabPage,
"audioio" => audioIOTabPage,
"paneq" => panEqTabPage,
"audioprofile" => audioProfileTabPage,
_ => null,
};
/// <summary>Rebuilds the main tab strip in the user's saved order, dropping the pan/EQ tab if that
/// preference is off. Preserves the current selection. Called at build and after Preferences close.</summary>
private void ApplyMainTabLayout()
{
var cfg = AppConfig.Load();
var order = NormalizeTabOrder(cfg.MainTabOrder);
bool showEq = cfg.ShowPanEqTab;
var selected = mainTabControl.SelectedTab;
mainTabControl.SuspendLayout();
try
{
int idx = mainTabControl.TabPages.IndexOf(audioProfileTabPage);
if (idx < 0) idx = mainTabControl.TabPages.Count;
mainTabControl.TabPages.Insert(idx, panEqTabPage);
}
else if (!show && present)
{
mainTabControl.TabPages.Remove(panEqTabPage);
mainTabControl.TabPages.Clear();
foreach (var key in order)
{
if (key == "paneq" && !showEq) continue;
if (TabPageForKey(key) is { } page) mainTabControl.TabPages.Add(page);
}
if (selected is not null && mainTabControl.TabPages.Contains(selected))
mainTabControl.SelectedTab = selected;
}
finally { mainTabControl.ResumeLayout(); }
}
/// <summary>Cleans a saved tab order: keep only known keys (in saved order, de-duplicated), then
/// append any known keys the saved list was missing, so the result always has all four.</summary>
private static List<string> NormalizeTabOrder(List<string>? saved)
{
var result = new List<string>();
if (saved is not null)
foreach (var k in saved)
if (Array.IndexOf(DefaultTabOrder, k) >= 0 && !result.Contains(k)) result.Add(k);
foreach (var k in DefaultTabOrder)
if (!result.Contains(k)) result.Add(k);
return result;
}
/// <summary>Rebuilds the peer picker from the currently-connected peers, one row per address. Runs
+98 -2
View File
@@ -287,6 +287,45 @@ internal sealed class PreferencesDialog : Form
AccessibleName = "Colour theme (Alt+T)",
};
// Appearance tab — reorder the main-window tabs, and toggle the Connectivity-tab peer lists.
private readonly ListBox tabOrderList = new()
{
Width = 320,
Height = 96,
IntegralHeight = false,
AccessibleName = "Main tab order (Alt+O), then Move up / Move down to reorder",
};
private readonly Button moveTabUpButton = new() { Text = "Move &up (Alt+U)", AutoSize = true, AccessibleName = "Move tab up" };
private readonly Button moveTabDownButton = new() { Text = "Move dow&n (Alt+N)", AutoSize = true, AccessibleName = "Move tab down" };
private readonly AccessibleCheckBox enableDiscoveredPeersBox = new()
{
Text = "Enable the &discovered peers list on the Connectivity tab (Alt+D)",
AccessibleName = "Enable the discovered peers list on the Connectivity tab",
AutoSize = true,
};
private readonly AccessibleCheckBox enableRememberedPeersBox = new()
{
Text = "Enable the &remembered peers list on the Connectivity tab (Alt+R)",
AccessibleName = "Enable the remembered peers list on the Connectivity tab",
AutoSize = true,
};
// The main-window tabs shown in the reorder list (always all four, even if the pan/EQ tab is
// currently hidden). Key matches AppConfig.MainTabOrder / MainForm.TabPageForKey.
private static readonly (string Key, string Display)[] KnownTabs =
[
("connectivity", "Connectivity"),
("audioio", "Audio inputs and outputs"),
("paneq", "Volume, pan and EQ for peers"),
("audioprofile", "Audio profile"),
];
private sealed class TabOrderItem(string key, string display)
{
public string Key { get; } = key;
public override string ToString() => display;
}
private readonly Label upnpStatusLabel = new()
{
Text = "",
@@ -649,6 +688,59 @@ internal sealed class PreferencesDialog : Form
themeRow.Controls.Add(themeBox);
themeRow.Controls.Add(themeHint);
// --- Appearance tab: reorder the main tabs, and toggle the Connectivity peer lists ---
// Populate the reorder list from the saved order, normalised so all four tabs always appear.
var orderedKeys = new List<string>();
if (cfgForLoad.MainTabOrder is { } savedOrder)
foreach (var k in savedOrder)
if (Array.Exists(KnownTabs, t => t.Key == k) && !orderedKeys.Contains(k)) orderedKeys.Add(k);
foreach (var (key, _) in KnownTabs)
if (!orderedKeys.Contains(key)) orderedKeys.Add(key);
foreach (var key in orderedKeys)
tabOrderList.Items.Add(new TabOrderItem(key, Array.Find(KnownTabs, t => t.Key == key).Display));
if (tabOrderList.Items.Count > 0) tabOrderList.SelectedIndex = 0;
void SaveTabOrder()
{
var cfg = AppConfig.Load();
cfg.MainTabOrder = tabOrderList.Items.Cast<TabOrderItem>().Select(t => t.Key).ToList();
try { cfg.Save(); } catch { /* harmless — order just won't survive a restart */ }
}
void MoveTab(int delta)
{
int i = tabOrderList.SelectedIndex;
if (i < 0) return;
int j = i + delta;
if (j < 0 || j >= tabOrderList.Items.Count) return;
var item = tabOrderList.Items[i];
tabOrderList.Items.RemoveAt(i);
tabOrderList.Items.Insert(j, item);
tabOrderList.SelectedIndex = j;
SaveTabOrder();
}
moveTabUpButton.Click += (_, _) => MoveTab(-1);
moveTabDownButton.Click += (_, _) => MoveTab(1);
var tabOrderLabel = new MnemonicLabel { Text = "Main tab &order (Alt+O)", AutoSize = true, Anchor = AnchorStyles.Left, MnemonicTarget = tabOrderList };
var tabOrderButtons = new FlowLayoutPanel { AutoSize = true, WrapContents = false, Margin = new Padding(0, 2, 0, 0) };
tabOrderButtons.Controls.Add(moveTabUpButton);
tabOrderButtons.Controls.Add(moveTabDownButton);
enableDiscoveredPeersBox.Checked = cfgForLoad.ShowDiscoveredPeers;
enableDiscoveredPeersBox.CheckedChanged += (_, _) =>
{
var cfg = AppConfig.Load();
cfg.ShowDiscoveredPeers = enableDiscoveredPeersBox.Checked;
try { cfg.Save(); } catch { /* harmless */ }
};
enableRememberedPeersBox.Checked = cfgForLoad.ShowRememberedPeers;
enableRememberedPeersBox.CheckedChanged += (_, _) =>
{
var cfg = AppConfig.Load();
cfg.ShowRememberedPeers = enableRememberedPeersBox.Checked;
try { cfg.Save(); } catch { /* harmless */ }
};
// 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.
@@ -837,12 +929,16 @@ internal sealed class PreferencesDialog : Form
pruneDaysRow.Controls.Add(pruneDaysBox);
pruneDaysRow.Controls.Add(pruneDaysUnitLabel);
// Five tabs, accessible (QuietTabControl) like the main window. Ctrl+Tab / arrows on the
// Six tabs (General, Appearance, Audio cues, Startup behaviour, Update settings, Logging),
// accessible (QuietTabControl) like the main window. Ctrl+Tab / arrows on the
// strip switch tabs; the active page's controls are the next tab stops. The control itself
// 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",
themeRow, browseProfilesFolderButton, acceptRemoteVolumeBox, upnpEnabledBox, upnpStatusLabel, showPanEqTabBox));
browseProfilesFolderButton, acceptRemoteVolumeBox, upnpEnabledBox, upnpStatusLabel));
tabs.TabPages.Add(MakeTab("Appearance",
themeRow, showPanEqTabBox, tabOrderLabel, tabOrderList, tabOrderButtons,
enableDiscoveredPeersBox, enableRememberedPeersBox));
tabs.TabPages.Add(MakeTab("Audio cues", cueGroup));
tabs.TabPages.Add(MakeTab("Startup behaviour",
startMinimisedBox, startWithUserBox, startWithProfileBox, startupListPanel));