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:
co-authored by
Claude Opus 4.8
parent
9c8e50667b
commit
78684f29f1
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -80,6 +80,16 @@ public sealed class AppConfig
|
||||
/// changing it takes effect on the next launch. Machine-wide, purely visual.</summary>
|
||||
public string ThemeMode { get; set; } = "system";
|
||||
|
||||
/// <summary>The order of the main-window tabs, as tab keys ("connectivity", "audioio", "paneq",
|
||||
/// "audioprofile"). Null / incomplete falls back to the default order. Set from the Preferences
|
||||
/// Appearance tab. Machine-wide.</summary>
|
||||
public List<string>? MainTabOrder { get; set; }
|
||||
|
||||
/// <summary>Whether the Discovered / Remembered peer lists appear on the Connectivity tab. Both on
|
||||
/// by default; toggled on the Preferences Appearance tab.</summary>
|
||||
public bool ShowDiscoveredPeers { get; set; } = true;
|
||||
public bool ShowRememberedPeers { get; set; } = true;
|
||||
|
||||
/// <summary>The machine-wide "named peers" book — peers the user has deliberately renamed, keyed by
|
||||
/// the peer's stable identity (machine name, or address for a nameless manual peer). A name applies in
|
||||
/// every profile and shows wherever that peer appears. Managed via the Connectivity tab's Rename peer
|
||||
|
||||
Reference in New Issue
Block a user