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:
Ednunp
2026-06-13 06:39:57 +01:00
co-authored by Claude Opus 4.8
parent 0bd3ef0018
commit 75e7432fcb
4 changed files with 197 additions and 355 deletions
+2 -12
View File
@@ -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);
+195 -65
View File
@@ -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
-1
View File
@@ -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,
-277
View File
@@ -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);
}
}