From 75e7432fcbcd3132eb9a9475ed18e8233d4183d6 Mon Sep 17 00:00:00 2001 From: Ednunp <29843396+Ednunp@users.noreply.github.com> Date: Sat, 13 Jun 2026 06:39:57 +0100 Subject: [PATCH] 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 --- src/RemSound.App/MainForm.cs | 14 +- src/RemSound.App/PreferencesDialog.cs | 260 ++++++++++++++----- src/RemSound.App/SelfTest.cs | 1 - src/RemSound.App/StartupBehaviourDialog.cs | 277 --------------------- 4 files changed, 197 insertions(+), 355 deletions(-) delete mode 100644 src/RemSound.App/StartupBehaviourDialog.cs diff --git a/src/RemSound.App/MainForm.cs b/src/RemSound.App/MainForm.cs index 8b45a4a..6de175d 100644 --- a/src/RemSound.App/MainForm.cs +++ b/src/RemSound.App/MainForm.cs @@ -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); diff --git a/src/RemSound.App/PreferencesDialog.cs b/src/RemSound.App/PreferencesDialog.cs index a6fc9a8..eaa15e5 100644 --- a/src/RemSound.App/PreferencesDialog.cs +++ b/src/RemSound.App/PreferencesDialog.cs @@ -4,27 +4,17 @@ using RemSound.Core; namespace RemSound.App; /// -/// 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 ; -/// 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 +/// 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 or /// 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 }; } + /// 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. + 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); + /// 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 diff --git a/src/RemSound.App/SelfTest.cs b/src/RemSound.App/SelfTest.cs index 564d891..bd2cae4 100644 --- a/src/RemSound.App/SelfTest.cs +++ b/src/RemSound.App/SelfTest.cs @@ -333,7 +333,6 @@ internal static class SelfTest { var factories = new (string Name, Func
Make)[] { - ("Startup behaviour", () => new StartupBehaviourDialog(null)), ("Recording settings", () => new RecordingSettingsDialog(new RecordingSettings())), ("Preferences", () => new PreferencesDialog( new RemSoundSettingsStore("RemSound"), null, diff --git a/src/RemSound.App/StartupBehaviourDialog.cs b/src/RemSound.App/StartupBehaviourDialog.cs deleted file mode 100644 index 37975a9..0000000 --- a/src/RemSound.App/StartupBehaviourDialog.cs +++ /dev/null @@ -1,277 +0,0 @@ -using RemSound.Core; - -namespace RemSound.App; - -/// -/// 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 (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. -/// -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); - } -}