Preferences: four accessible tabs (General / Audio cues / Startup behaviour / Update settings)
The Preferences dialog is now a QuietTabControl with four tabs (same accessible tab approach as the main window; Ctrl+Tab / arrows switch tabs): - General: profiles-folder browse, accept-remote-volume, UPnP, enable logs / write logs now. - Audio cues: the redesigned cue UI (plain cue list + "(none)" sound option) + keyboard clicks. - Startup behaviour: Start minimised / Start with Windows / Start with a specific profile - moved here from the standalone Options-menu dialog, wiring and persistence unchanged (AppConfig + the Windows auto-start registry entry). - Update settings: startup-check, frequency, check-now, silent-install, show-what's-new. Removed the Options-menu "Startup behaviour" item and deleted the now-unused StartupBehaviourDialog.cs (and dropped it from the self-test's accessibility audit). The audit still passes on the tabbed dialog with no mnemonic clashes (Alt-letters are isolated per tab). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0bd3ef0018
commit
75e7432fcb
@@ -1604,17 +1604,8 @@ public sealed class MainForm : Form
|
||||
};
|
||||
keyboardItem.Click += (_, _) => hotkeyController.ShowKeyboardShortcutsDialog(this);
|
||||
|
||||
var startupBehaviourItem = new ToolStripMenuItem("S&tartup behaviour...")
|
||||
{
|
||||
AccessibleName = "Startup behaviour",
|
||||
};
|
||||
startupBehaviourItem.Click += (_, _) =>
|
||||
{
|
||||
using var dialog = new StartupBehaviourDialog(profileStore);
|
||||
dialog.ShowDialog(this);
|
||||
// Startup-behaviour state persists through AppConfig / registry directly. No
|
||||
// profile-dirty flag involved here — none of these settings live on Profile.
|
||||
};
|
||||
// Startup behaviour moved into the Preferences dialog (its own tab) on 2026-06-13; the
|
||||
// Options-menu item and the standalone StartupBehaviourDialog are retired.
|
||||
|
||||
var prefsItem = new ToolStripMenuItem("&Preferences...")
|
||||
{
|
||||
@@ -1648,7 +1639,6 @@ public sealed class MainForm : Form
|
||||
{
|
||||
recordingSettingsItem,
|
||||
keyboardItem,
|
||||
startupBehaviourItem,
|
||||
profilePasswordsItem,
|
||||
};
|
||||
if (realtekToggle is not null) optionItems.Add(realtekToggle);
|
||||
|
||||
@@ -4,27 +4,17 @@ using RemSound.Core;
|
||||
namespace RemSound.App;
|
||||
|
||||
/// <summary>
|
||||
/// Preferences dialog. Holds settings that aren't profile-management actions in their own
|
||||
/// right:
|
||||
/// * Browse for RemSound profiles folder — picks the directory the profile picker scans
|
||||
/// next launch.
|
||||
/// * Audio cue sounds — per-cue enable list (connect, disconnect, recording start/stop).
|
||||
/// One CheckedListBox; ticked items play, unticked are silent. Replaced the old single
|
||||
/// "Mute connect/disconnect sounds" toggle (2026-05-15) when recording start/stop cues
|
||||
/// were added — a CheckedListBox scales to future cues without dialog re-layout. Label
|
||||
/// gained the "Audio" prefix on 2026-05-21 to disambiguate from the underlying engine's
|
||||
/// "buffer cues" and "ASIO cues" diagnostic terms, which look the same in writing.
|
||||
/// * Accept remote volume commands from peers — opt-in for the remote-control feature.
|
||||
/// * Update settings — startup-check toggle, frequency, manual check, silent-install
|
||||
/// toggle. Layout deliberately reads top-to-bottom as the question the user is
|
||||
/// answering: "Check for updates on startup? (yes/no) Then, in the background, every?
|
||||
/// (interval) When one's found? (silent install / ask first)".
|
||||
/// * UPnP — optional automatic router port-forwarding via Mono.Nat. Off by default; when
|
||||
/// ticked, we kick off discovery and surface the result + external address inline.
|
||||
/// * Enable logs + Write logs now.
|
||||
///
|
||||
/// Startup behaviour was previously a button here that opened <see cref="StartupBehaviourDialog"/>;
|
||||
/// it's now a top-level Options menu item in its own right (2026-05-15 menu reorg).
|
||||
/// Preferences dialog. A four-tab dialog (2026-06-13 overhaul) using the same accessible
|
||||
/// <see cref="QuietTabControl"/> as the main window:
|
||||
/// * General — Browse for RemSound profiles folder, Accept remote volume commands, UPnP
|
||||
/// automatic router port-forwarding, and Enable logs / Write logs now.
|
||||
/// * Audio cues — the cue list (a plain list of cue names; arrowing previews each cue's
|
||||
/// current sound), a "Choose sound" list whose "(none)" entry turns a cue off, the Play /
|
||||
/// Browse actions, and the keyboard-clicks toggle.
|
||||
/// * Startup behaviour — Start minimised, Start with Windows, Start with a specific profile
|
||||
/// (+ the profile list). Moved here from the standalone Options-menu dialog.
|
||||
/// * Update settings — startup-check toggle, frequency, manual check, silent-install, and
|
||||
/// show-what's-new.
|
||||
///
|
||||
/// All settings save through <see cref="RemSoundSettingsStore"/> or <see cref="AppConfig"/>
|
||||
/// on every change (no OK-to-commit). Esc or Close dismisses.
|
||||
@@ -298,6 +288,41 @@ internal sealed class PreferencesDialog : Form
|
||||
AutoSize = true,
|
||||
};
|
||||
|
||||
// Startup behaviour (moved here from the Options-menu StartupBehaviourDialog, 2026-06-13). These
|
||||
// live on their own tab; their Alt-letters are isolated per tab so reusing M/A/P/L is fine.
|
||||
private readonly AccessibleCheckBox startMinimisedBox = new()
|
||||
{
|
||||
Text = "Start minimised to tray (Alt+&M)",
|
||||
AccessibleName = "Start minimised to tray",
|
||||
AutoSize = true,
|
||||
};
|
||||
private readonly AccessibleCheckBox startWithUserBox = new()
|
||||
{
|
||||
Text = "Start RemSound automatically when this user logs in (Alt+&A)",
|
||||
AccessibleName = "Start RemSound automatically when this user logs in",
|
||||
AutoSize = true,
|
||||
};
|
||||
private readonly AccessibleCheckBox startWithProfileBox = new()
|
||||
{
|
||||
Text = "Start with a specific profile (Alt+&P)",
|
||||
AccessibleName = "Start with a specific profile",
|
||||
AutoSize = true,
|
||||
};
|
||||
private readonly Label startupProfileListLabel = new()
|
||||
{
|
||||
Text = "Profile to start with (Alt+&L):",
|
||||
AutoSize = true,
|
||||
AccessibleName = "Profile to start with",
|
||||
};
|
||||
private readonly ListBox startupProfileList = new()
|
||||
{
|
||||
IntegralHeight = false,
|
||||
Width = 360,
|
||||
Height = 120,
|
||||
AccessibleName = "Profile to start with",
|
||||
};
|
||||
private bool suppressStartWithUserHandler;
|
||||
|
||||
private readonly Button closeButton = new()
|
||||
{
|
||||
Text = "Close",
|
||||
@@ -549,37 +574,8 @@ internal sealed class PreferencesDialog : Form
|
||||
|
||||
closeButton.Click += (_, _) => Close();
|
||||
|
||||
var panel = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
Padding = new Padding(12),
|
||||
ColumnCount = 1,
|
||||
RowCount = 13,
|
||||
};
|
||||
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
for (var i = 0; i < 12; i++) panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
|
||||
// Tab order top-to-bottom: browse-profiles-folder → cue list → Play selected →
|
||||
// Browse for selected → accept remote → check-on-startup → frequency → check-now →
|
||||
// silent install → UPnP → enable logs → write logs now → close. The cue section is
|
||||
// three tab stops total: the list itself (where up/down navigates between cues and
|
||||
// Space toggles enable), then the two action buttons that operate on whichever cue
|
||||
// is currently selected in the list.
|
||||
browseProfilesFolderButton.TabIndex = 0;
|
||||
cueList.TabIndex = 1;
|
||||
playSelectedCueButton.TabIndex = 2;
|
||||
browseSelectedCueButton.TabIndex = 3;
|
||||
acceptRemoteVolumeBox.TabIndex = 4;
|
||||
checkForUpdatesOnStartupBox.TabIndex = 5;
|
||||
updateFrequencyBox.TabIndex = 6;
|
||||
checkForUpdatesNowButton.TabIndex = 7;
|
||||
silentlyInstallUpdatesBox.TabIndex = 8;
|
||||
showWhatsNewAfterUpdateBox.TabIndex = 9;
|
||||
upnpEnabledBox.TabIndex = 10;
|
||||
loggingBox.TabIndex = 11;
|
||||
writeLogsNowButton.TabIndex = 12;
|
||||
closeButton.TabIndex = 13;
|
||||
// Wire up the Startup behaviour tab (moved here from the old Options-menu dialog).
|
||||
WireStartupBehaviour(profileStore);
|
||||
|
||||
// Group the frequency label + combo on one FlowLayoutPanel row so the visible label
|
||||
// sits inline next to the combo while keeping the combo as the focusable target.
|
||||
@@ -630,18 +626,27 @@ internal sealed class PreferencesDialog : Form
|
||||
cueGroup.Controls.Add(cueActions, 0, 4);
|
||||
cueGroup.Controls.Add(keyboardClicksBox, 0, 5);
|
||||
|
||||
panel.Controls.Add(browseProfilesFolderButton, 0, 0);
|
||||
panel.Controls.Add(cueGroup, 0, 1);
|
||||
panel.Controls.Add(acceptRemoteVolumeBox, 0, 2);
|
||||
panel.Controls.Add(checkForUpdatesOnStartupBox, 0, 3);
|
||||
panel.Controls.Add(freqRow, 0, 4);
|
||||
panel.Controls.Add(checkForUpdatesNowButton, 0, 5);
|
||||
panel.Controls.Add(silentlyInstallUpdatesBox, 0, 6);
|
||||
panel.Controls.Add(showWhatsNewAfterUpdateBox, 0, 7);
|
||||
panel.Controls.Add(upnpEnabledBox, 0, 8);
|
||||
panel.Controls.Add(upnpStatusLabel, 0, 9);
|
||||
panel.Controls.Add(loggingBox, 0, 10);
|
||||
panel.Controls.Add(writeLogsNowButton, 0, 11);
|
||||
// Startup profile list (label + list), shown only when "start with a specific profile" is on.
|
||||
var startupListPanel = new FlowLayoutPanel
|
||||
{
|
||||
FlowDirection = FlowDirection.TopDown,
|
||||
AutoSize = true,
|
||||
WrapContents = false,
|
||||
Padding = new Padding(20, 0, 0, 0),
|
||||
};
|
||||
startupListPanel.Controls.Add(startupProfileListLabel);
|
||||
startupListPanel.Controls.Add(startupProfileList);
|
||||
|
||||
// Four tabs, accessible (QuietTabControl) like the main window. Ctrl+Tab / arrows on the
|
||||
// strip switch tabs; the active page's controls are the next tab stops.
|
||||
var tabs = new QuietTabControl { Dock = DockStyle.Fill, TabIndex = 0, TabStop = true };
|
||||
tabs.TabPages.Add(MakeTab("General",
|
||||
browseProfilesFolderButton, acceptRemoteVolumeBox, upnpEnabledBox, upnpStatusLabel, loggingBox, writeLogsNowButton));
|
||||
tabs.TabPages.Add(MakeTab("Audio cues", cueGroup));
|
||||
tabs.TabPages.Add(MakeTab("Startup behaviour",
|
||||
startMinimisedBox, startWithUserBox, startWithProfileBox, startupListPanel));
|
||||
tabs.TabPages.Add(MakeTab("Update settings",
|
||||
checkForUpdatesOnStartupBox, freqRow, checkForUpdatesNowButton, silentlyInstallUpdatesBox, showWhatsNewAfterUpdateBox));
|
||||
|
||||
var buttons = new FlowLayoutPanel
|
||||
{
|
||||
@@ -652,9 +657,30 @@ internal sealed class PreferencesDialog : Form
|
||||
};
|
||||
buttons.Controls.Add(closeButton);
|
||||
|
||||
Controls.Add(panel);
|
||||
Controls.Add(tabs);
|
||||
Controls.Add(buttons);
|
||||
|
||||
// Stack the given controls vertically in a tab body (auto-size rows + a spacer), mirroring
|
||||
// the original single-panel layout so Dock=Fill children like the cue group still fit.
|
||||
static TabPage MakeTab(string title, params Control[] controls)
|
||||
{
|
||||
var page = new TabPage(title);
|
||||
var body = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
Padding = new Padding(12),
|
||||
ColumnCount = 1,
|
||||
RowCount = controls.Length + 1,
|
||||
AutoScroll = true,
|
||||
};
|
||||
body.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
for (var i = 0; i < controls.Length; i++) body.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
body.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
for (var i = 0; i < controls.Length; i++) body.Controls.Add(controls[i], 0, i);
|
||||
page.Controls.Add(body);
|
||||
return page;
|
||||
}
|
||||
|
||||
AcceptButton = closeButton;
|
||||
CancelButton = closeButton;
|
||||
|
||||
@@ -669,6 +695,110 @@ internal sealed class PreferencesDialog : Form
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Wire up the Startup behaviour tab (moved here from StartupBehaviourDialog): load the
|
||||
/// current state, populate the profile list, and persist each change immediately to AppConfig /
|
||||
/// the Windows auto-start registry entry, exactly as the old dialog did.</summary>
|
||||
private void WireStartupBehaviour(ProfileStore? store)
|
||||
{
|
||||
var cfg = AppConfig.Load();
|
||||
startMinimisedBox.Checked = cfg.StartMinimised;
|
||||
startWithUserBox.Checked = StartupAutoStart.IsEnabled;
|
||||
var hasProfile = !string.IsNullOrWhiteSpace(cfg.StartWithProfileTitle);
|
||||
startWithProfileBox.Checked = hasProfile;
|
||||
|
||||
if (store is not null)
|
||||
{
|
||||
foreach (var title in store.ListProfileTitles()) startupProfileList.Items.Add(title);
|
||||
}
|
||||
if (hasProfile && cfg.StartWithProfileTitle is { } savedTitle)
|
||||
{
|
||||
var idx = startupProfileList.Items.IndexOf(savedTitle);
|
||||
if (idx >= 0) startupProfileList.SelectedIndex = idx;
|
||||
}
|
||||
UpdateStartupProfileListVisibility();
|
||||
|
||||
startMinimisedBox.CheckedChanged += (_, _) =>
|
||||
{
|
||||
var c = AppConfig.Load();
|
||||
c.StartMinimised = startMinimisedBox.Checked;
|
||||
try { c.Save(); } catch (Exception ex) { ShowStartupWarning("Could not save Start minimised preference: " + ex.Message); }
|
||||
};
|
||||
|
||||
startWithUserBox.CheckedChanged += (_, _) =>
|
||||
{
|
||||
if (suppressStartWithUserHandler) return;
|
||||
// Source of truth for auto-start is the registry, not AppConfig — flip it directly.
|
||||
var ok = startWithUserBox.Checked ? StartupAutoStart.TryEnable() : StartupAutoStart.TryDisable();
|
||||
if (!ok)
|
||||
{
|
||||
MessageBox.Show(this,
|
||||
"RemSound could not change the auto-start setting in the Windows registry. The setting did not change. (This usually means a policy or another security tool is blocking it.)",
|
||||
"Auto-start change failed", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
var actual = StartupAutoStart.IsEnabled;
|
||||
if (startWithUserBox.Checked != actual)
|
||||
{
|
||||
suppressStartWithUserHandler = true;
|
||||
try { startWithUserBox.Checked = actual; }
|
||||
finally { suppressStartWithUserHandler = false; }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
startWithProfileBox.CheckedChanged += (_, _) =>
|
||||
{
|
||||
UpdateStartupProfileListVisibility();
|
||||
if (startWithProfileBox.Checked)
|
||||
{
|
||||
if (startupProfileList.Items.Count == 0)
|
||||
{
|
||||
MessageBox.Show(this,
|
||||
"You don't have any saved profiles yet. Save a profile first (File menu -> Save profile as), then come back here and pick it.",
|
||||
"No saved profiles", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
startWithProfileBox.Checked = false;
|
||||
return;
|
||||
}
|
||||
if (startupProfileList.SelectedIndex < 0) startupProfileList.SelectedIndex = 0;
|
||||
CommitStartupProfileSelection();
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearStartupProfileSelection();
|
||||
}
|
||||
};
|
||||
|
||||
startupProfileList.SelectedIndexChanged += (_, _) =>
|
||||
{
|
||||
if (!startWithProfileBox.Checked || startupProfileList.SelectedIndex < 0) return;
|
||||
CommitStartupProfileSelection();
|
||||
};
|
||||
startupProfileListLabel.Click += (_, _) => startupProfileList.Focus();
|
||||
}
|
||||
|
||||
private void UpdateStartupProfileListVisibility()
|
||||
{
|
||||
var visible = startWithProfileBox.Checked;
|
||||
startupProfileListLabel.Visible = visible;
|
||||
startupProfileList.Visible = visible;
|
||||
}
|
||||
|
||||
private void CommitStartupProfileSelection()
|
||||
{
|
||||
if (startupProfileList.SelectedItem is not string title || string.IsNullOrWhiteSpace(title)) return;
|
||||
var c = AppConfig.Load();
|
||||
c.StartWithProfileTitle = title;
|
||||
try { c.Save(); } catch (Exception ex) { ShowStartupWarning("Could not save the start-with-profile choice: " + ex.Message); }
|
||||
}
|
||||
|
||||
private void ClearStartupProfileSelection()
|
||||
{
|
||||
var c = AppConfig.Load();
|
||||
c.StartWithProfileTitle = null;
|
||||
try { c.Save(); } catch (Exception ex) { ShowStartupWarning("Could not save the start-with-profile choice: " + ex.Message); }
|
||||
}
|
||||
|
||||
private void ShowStartupWarning(string message) =>
|
||||
MessageBox.Show(this, message, "Startup behaviour", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
|
||||
/// <summary>Refresh the Play and Browse action buttons so their visible text and
|
||||
/// AccessibleName reflect the currently-selected cue. Called on every selection change
|
||||
/// in the cue listbox AND immediately after a Browse pick (the "(custom)" tag flips
|
||||
|
||||
@@ -333,7 +333,6 @@ internal static class SelfTest
|
||||
{
|
||||
var factories = new (string Name, Func<Form> Make)[]
|
||||
{
|
||||
("Startup behaviour", () => new StartupBehaviourDialog(null)),
|
||||
("Recording settings", () => new RecordingSettingsDialog(new RecordingSettings())),
|
||||
("Preferences", () => new PreferencesDialog(
|
||||
new RemSoundSettingsStore("RemSound"), null,
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
using RemSound.Core;
|
||||
|
||||
namespace RemSound.App;
|
||||
|
||||
/// <summary>
|
||||
/// Modal dialog for the three "what should RemSound do at launch" toggles:
|
||||
/// * Start minimised — main window goes to the tray immediately after Show.
|
||||
/// * Start RemSound automatically with this user — wires HKCU\...\Run.
|
||||
/// * Start with a specific profile — skips the startup picker and loads the chosen
|
||||
/// profile directly. Companion listbox of saved profiles appears alongside.
|
||||
///
|
||||
/// The dialog persists changes through <see cref="AppConfig"/> (StartMinimised /
|
||||
/// StartWithProfileTitle) and through the Windows registry (the auto-start checkbox).
|
||||
/// Each change is committed immediately, no OK/Apply button — same per-tick-saves
|
||||
/// pattern as the rest of the Profiles and preferences tab.
|
||||
///
|
||||
/// Keyboard shape (per Ed's spec):
|
||||
/// * Tab cycles: minimised checkbox → auto-start checkbox → specific-profile
|
||||
/// checkbox → profiles list (when shown) → Close button.
|
||||
/// * Esc or the Close button closes.
|
||||
/// * Each checkbox has an Alt+letter mnemonic.
|
||||
/// </summary>
|
||||
internal sealed class StartupBehaviourDialog : Form
|
||||
{
|
||||
private readonly AccessibleCheckBox startMinimisedBox = new()
|
||||
{
|
||||
Text = "Start minimised to tray (Alt+&M)",
|
||||
AccessibleName = "Start minimised to tray",
|
||||
AutoSize = true,
|
||||
};
|
||||
private readonly AccessibleCheckBox startWithUserBox = new()
|
||||
{
|
||||
Text = "Start RemSound automatically when this user logs in (Alt+&A)",
|
||||
AccessibleName = "Start RemSound automatically when this user logs in",
|
||||
AutoSize = true,
|
||||
};
|
||||
private readonly AccessibleCheckBox startWithProfileBox = new()
|
||||
{
|
||||
Text = "Start with a specific profile (Alt+&P)",
|
||||
AccessibleName = "Start with a specific profile",
|
||||
AutoSize = true,
|
||||
};
|
||||
private readonly Label profileListLabel = new()
|
||||
{
|
||||
Text = "Profile to start with (Alt+&L):",
|
||||
AutoSize = true,
|
||||
AccessibleName = "Profile to start with",
|
||||
};
|
||||
private readonly ListBox profileList = new()
|
||||
{
|
||||
IntegralHeight = false,
|
||||
Width = 360,
|
||||
Height = 140,
|
||||
AccessibleName = "Profile to start with",
|
||||
};
|
||||
private readonly Button closeButton = new()
|
||||
{
|
||||
Text = "Close",
|
||||
AutoSize = true,
|
||||
DialogResult = DialogResult.OK,
|
||||
AccessibleName = "Close",
|
||||
};
|
||||
|
||||
public StartupBehaviourDialog(ProfileStore? profileStore)
|
||||
{
|
||||
Text = "Startup behaviour";
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MinimizeBox = false;
|
||||
MaximizeBox = false;
|
||||
ShowInTaskbar = false;
|
||||
KeyPreview = true; // form-level Esc handler
|
||||
ClientSize = new Size(540, 360);
|
||||
|
||||
// === Layout ===
|
||||
var root = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
Padding = new Padding(12),
|
||||
ColumnCount = 1,
|
||||
RowCount = 6, // 0 intro, 1 minimise, 2 auto-start, 3 specific-profile, 4 list (with label), 5 close
|
||||
};
|
||||
root.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
for (var i = 0; i < 5; i++) root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
|
||||
var intro = new Label
|
||||
{
|
||||
Text = "These options control what RemSound does when it launches. They persist across sessions and affect every launch (whether started by the user or by Windows on login).",
|
||||
AutoSize = true,
|
||||
MaximumSize = new Size(500, 0),
|
||||
Anchor = AnchorStyles.Left,
|
||||
};
|
||||
root.Controls.Add(intro, 0, 0);
|
||||
|
||||
// Each checkbox lives on its own row, plus the profile list row when relevant.
|
||||
root.Controls.Add(startMinimisedBox, 0, 1);
|
||||
root.Controls.Add(startWithUserBox, 0, 2);
|
||||
root.Controls.Add(startWithProfileBox, 0, 3);
|
||||
|
||||
// Profile list: label + list stacked, hidden until startWithProfileBox is ticked.
|
||||
// FlowLayoutPanel keeps them tidy and the whole sub-block can be toggled visible
|
||||
// as a unit.
|
||||
var listSubPanel = new FlowLayoutPanel
|
||||
{
|
||||
FlowDirection = FlowDirection.TopDown,
|
||||
AutoSize = true,
|
||||
WrapContents = false,
|
||||
Padding = new Padding(20, 0, 0, 0), // indent slightly so it visually belongs to the checkbox above
|
||||
};
|
||||
listSubPanel.Controls.Add(profileListLabel);
|
||||
listSubPanel.Controls.Add(profileList);
|
||||
root.Controls.Add(listSubPanel, 0, 4);
|
||||
|
||||
var closePanel = new FlowLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
FlowDirection = FlowDirection.RightToLeft,
|
||||
AutoSize = true,
|
||||
Padding = new Padding(0, 8, 0, 0),
|
||||
};
|
||||
closePanel.Controls.Add(closeButton);
|
||||
root.Controls.Add(closePanel, 0, 5);
|
||||
|
||||
Controls.Add(root);
|
||||
|
||||
// === Tab order ===
|
||||
startMinimisedBox.TabIndex = 0;
|
||||
startWithUserBox.TabIndex = 1;
|
||||
startWithProfileBox.TabIndex = 2;
|
||||
profileList.TabIndex = 3;
|
||||
closeButton.TabIndex = 4;
|
||||
|
||||
// === Initial state ===
|
||||
var cfg = AppConfig.Load();
|
||||
startMinimisedBox.Checked = cfg.StartMinimised;
|
||||
startWithUserBox.Checked = StartupAutoStart.IsEnabled;
|
||||
var hasProfile = !string.IsNullOrWhiteSpace(cfg.StartWithProfileTitle);
|
||||
startWithProfileBox.Checked = hasProfile;
|
||||
|
||||
// Populate profile list and select the saved choice if any.
|
||||
if (profileStore is not null)
|
||||
{
|
||||
foreach (var title in profileStore.ListProfileTitles())
|
||||
{
|
||||
profileList.Items.Add(title);
|
||||
}
|
||||
}
|
||||
if (hasProfile && cfg.StartWithProfileTitle is { } savedTitle)
|
||||
{
|
||||
var idx = profileList.Items.IndexOf(savedTitle);
|
||||
if (idx >= 0) profileList.SelectedIndex = idx;
|
||||
}
|
||||
|
||||
UpdateProfileListVisibility();
|
||||
|
||||
// === Wiring ===
|
||||
startMinimisedBox.CheckedChanged += (_, _) =>
|
||||
{
|
||||
var c = AppConfig.Load();
|
||||
c.StartMinimised = startMinimisedBox.Checked;
|
||||
try { c.Save(); } catch (Exception ex) { ShowSaveWarning("Could not save Start minimised preference: " + ex.Message); }
|
||||
};
|
||||
|
||||
var suppressStartWithUserHandler = false;
|
||||
startWithUserBox.CheckedChanged += (_, _) =>
|
||||
{
|
||||
if (suppressStartWithUserHandler) return;
|
||||
// Source of truth for the auto-start state is the registry — we don't keep a
|
||||
// duplicate in AppConfig. So this just flips the registry entry directly.
|
||||
var ok = startWithUserBox.Checked
|
||||
? StartupAutoStart.TryEnable()
|
||||
: StartupAutoStart.TryDisable();
|
||||
if (!ok)
|
||||
{
|
||||
MessageBox.Show(this,
|
||||
"RemSound could not change the auto-start setting in the Windows registry. The setting did not change. (This usually means a policy or another security tool is blocking it.)",
|
||||
"Auto-start change failed",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
// Re-read truth and reflect it WITHOUT re-firing this handler. The suppress flag
|
||||
// genuinely gates it; the old detach/re-attach targeted an empty handler that was
|
||||
// never in the invocation list, so it did nothing and the corrective set re-fired.
|
||||
var actual = StartupAutoStart.IsEnabled;
|
||||
if (startWithUserBox.Checked != actual)
|
||||
{
|
||||
suppressStartWithUserHandler = true;
|
||||
try { startWithUserBox.Checked = actual; }
|
||||
finally { suppressStartWithUserHandler = false; }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
startWithProfileBox.CheckedChanged += (_, _) =>
|
||||
{
|
||||
UpdateProfileListVisibility();
|
||||
if (startWithProfileBox.Checked)
|
||||
{
|
||||
if (profileList.Items.Count == 0)
|
||||
{
|
||||
MessageBox.Show(this,
|
||||
"You don't have any saved profiles yet. Save a profile first (Profiles and preferences tab → Save profile as), then come back here and pick it.",
|
||||
"No saved profiles",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
// Untick without re-firing.
|
||||
startWithProfileBox.Checked = false;
|
||||
return;
|
||||
}
|
||||
if (profileList.SelectedIndex < 0) profileList.SelectedIndex = 0;
|
||||
CommitProfileSelection();
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearProfileSelection();
|
||||
}
|
||||
};
|
||||
|
||||
profileList.SelectedIndexChanged += (_, _) =>
|
||||
{
|
||||
if (!startWithProfileBox.Checked) return;
|
||||
if (profileList.SelectedIndex < 0) return;
|
||||
CommitProfileSelection();
|
||||
};
|
||||
profileList.DoubleClick += (_, _) =>
|
||||
{
|
||||
// Same effect as picking a row + closing — convenient for mouse users.
|
||||
if (startWithProfileBox.Checked && profileList.SelectedIndex >= 0)
|
||||
{
|
||||
CommitProfileSelection();
|
||||
}
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
};
|
||||
|
||||
KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.KeyCode == Keys.Escape)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
Close();
|
||||
e.SuppressKeyPress = true;
|
||||
e.Handled = true;
|
||||
}
|
||||
};
|
||||
AcceptButton = closeButton;
|
||||
CancelButton = closeButton;
|
||||
Load += (_, _) => startMinimisedBox.Focus();
|
||||
}
|
||||
|
||||
private void UpdateProfileListVisibility()
|
||||
{
|
||||
var visible = startWithProfileBox.Checked;
|
||||
profileListLabel.Visible = visible;
|
||||
profileList.Visible = visible;
|
||||
}
|
||||
|
||||
private void CommitProfileSelection()
|
||||
{
|
||||
if (profileList.SelectedItem is not string title || string.IsNullOrWhiteSpace(title)) return;
|
||||
var c = AppConfig.Load();
|
||||
c.StartWithProfileTitle = title;
|
||||
try { c.Save(); } catch (Exception ex) { ShowSaveWarning("Could not save the start-with-profile choice: " + ex.Message); }
|
||||
}
|
||||
|
||||
private void ClearProfileSelection()
|
||||
{
|
||||
var c = AppConfig.Load();
|
||||
c.StartWithProfileTitle = null;
|
||||
try { c.Save(); } catch (Exception ex) { ShowSaveWarning("Could not save the start-with-profile choice: " + ex.Message); }
|
||||
}
|
||||
|
||||
private void ShowSaveWarning(string message)
|
||||
{
|
||||
MessageBox.Show(this, message, "Startup behaviour", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user