Skip to content

Commit f1b61ae

Browse files
authored
feat: add startup and behavior settings (#14)
- Add Settings tab with four configurable options: - Launch on startup (registers app in Windows startup) - Start minimized (start hidden to system tray) - Minimize to tray (hide to tray instead of taskbar) - Close to tray (hide to tray instead of closing) - Add SettingsService for persisting settings via JSON file - Override ToggleSwitch accent color to match purple theme - Settings stored in %LOCALAPPDATA%\CodingWithCalvin.VSToolbox Closes #2
1 parent de1d96a commit f1b61ae

5 files changed

Lines changed: 349 additions & 19 deletions

File tree

src/CodingWithCalvin.VSToolbox/App.xaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
1515
</ResourceDictionary.MergedDictionaries>
1616

17+
<!-- Override ToggleSwitch accent color to purple -->
18+
<SolidColorBrush x:Key="ToggleSwitchFillOn" Color="#68217A"/>
19+
<SolidColorBrush x:Key="ToggleSwitchFillOnPointerOver" Color="#7B2A91"/>
20+
<SolidColorBrush x:Key="ToggleSwitchFillOnPressed" Color="#551A66"/>
21+
<SolidColorBrush x:Key="ToggleSwitchFillOnDisabled" Color="#4D68217A"/>
22+
1723
<!-- Converters -->
1824
<converters:NullToBoolConverter x:Key="NullToBoolConverter"/>
1925
<converters:FilePathToImageConverter x:Key="FilePathToImageConverter"/>

src/CodingWithCalvin.VSToolbox/App.xaml.cs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ public partial class App : Application
6565
private Window? _window;
6666
private AppWindow? _appWindow;
6767
private TrayIconService? _trayIconService;
68+
private SettingsService? _settingsService;
6869

6970
public Window? MainWindow => _window;
71+
public SettingsService Settings => _settingsService ??= new SettingsService();
7072

7173
public App()
7274
{
@@ -122,7 +124,11 @@ protected override void OnLaunched(LaunchActivatedEventArgs e)
122124
// Set window size and position to bottom-right
123125
PositionWindowBottomRight(540, 600);
124126

125-
_window.Activate();
127+
// Only show window if not starting minimized
128+
if (!Settings.StartMinimized)
129+
{
130+
_window.Activate();
131+
}
126132
}
127133

128134
private void ConfigureCustomTitleBar()
@@ -178,11 +184,21 @@ private void PositionWindowBottomRight(int width, int height)
178184

179185
private void OnAppWindowClosing(AppWindow sender, AppWindowClosingEventArgs args)
180186
{
181-
// Prevent window from closing - hide it instead
182-
args.Cancel = true;
187+
if (Settings.CloseToTray)
188+
{
189+
// Prevent window from closing - hide it instead
190+
args.Cancel = true;
183191

184-
// Hide the window
185-
_appWindow?.Hide();
192+
// Hide the window
193+
_appWindow?.Hide();
194+
}
195+
else
196+
{
197+
// Actually close the app
198+
_trayIconService?.Dispose();
199+
_mutex?.ReleaseMutex();
200+
_mutex?.Dispose();
201+
}
186202
}
187203

188204
private void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
using System.Text.Json;
2+
using Microsoft.Win32;
3+
4+
namespace CodingWithCalvin.VSToolbox.Services;
5+
6+
public class SettingsService
7+
{
8+
private const string StartupRegistryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
9+
private const string AppName = "VSToolbox";
10+
private const string SettingsFileName = "settings.json";
11+
12+
private static readonly string SettingsFilePath = Path.Combine(
13+
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
14+
"CodingWithCalvin.VSToolbox",
15+
SettingsFileName);
16+
17+
private static SettingsData? _settings;
18+
private static readonly object _lock = new();
19+
20+
private static SettingsData Settings
21+
{
22+
get
23+
{
24+
if (_settings is null)
25+
{
26+
lock (_lock)
27+
{
28+
_settings ??= LoadSettings();
29+
}
30+
}
31+
return _settings;
32+
}
33+
}
34+
35+
public bool LaunchOnStartup
36+
{
37+
get => Settings.LaunchOnStartup;
38+
set
39+
{
40+
Settings.LaunchOnStartup = value;
41+
SaveSettings();
42+
UpdateStartupRegistration(value);
43+
}
44+
}
45+
46+
public bool StartMinimized
47+
{
48+
get => Settings.StartMinimized;
49+
set
50+
{
51+
Settings.StartMinimized = value;
52+
SaveSettings();
53+
}
54+
}
55+
56+
public bool MinimizeToTray
57+
{
58+
get => Settings.MinimizeToTray;
59+
set
60+
{
61+
Settings.MinimizeToTray = value;
62+
SaveSettings();
63+
}
64+
}
65+
66+
public bool CloseToTray
67+
{
68+
get => Settings.CloseToTray;
69+
set
70+
{
71+
Settings.CloseToTray = value;
72+
SaveSettings();
73+
}
74+
}
75+
76+
private static SettingsData LoadSettings()
77+
{
78+
try
79+
{
80+
if (File.Exists(SettingsFilePath))
81+
{
82+
var json = File.ReadAllText(SettingsFilePath);
83+
return JsonSerializer.Deserialize<SettingsData>(json) ?? new SettingsData();
84+
}
85+
}
86+
catch
87+
{
88+
// If we can't load settings, return defaults
89+
}
90+
return new SettingsData();
91+
}
92+
93+
private static void SaveSettings()
94+
{
95+
try
96+
{
97+
var directory = Path.GetDirectoryName(SettingsFilePath);
98+
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
99+
{
100+
Directory.CreateDirectory(directory);
101+
}
102+
103+
var json = JsonSerializer.Serialize(_settings, new JsonSerializerOptions { WriteIndented = true });
104+
File.WriteAllText(SettingsFilePath, json);
105+
}
106+
catch
107+
{
108+
// Silently fail if we can't save settings
109+
}
110+
}
111+
112+
private static void UpdateStartupRegistration(bool enable)
113+
{
114+
try
115+
{
116+
using var key = Registry.CurrentUser.OpenSubKey(StartupRegistryKey, writable: true);
117+
if (key is null) return;
118+
119+
if (enable)
120+
{
121+
var exePath = Environment.ProcessPath;
122+
if (!string.IsNullOrEmpty(exePath))
123+
{
124+
key.SetValue(AppName, $"\"{exePath}\"");
125+
}
126+
}
127+
else
128+
{
129+
key.DeleteValue(AppName, throwOnMissingValue: false);
130+
}
131+
}
132+
catch
133+
{
134+
// Silently fail if we can't access the registry
135+
}
136+
}
137+
138+
/// <summary>
139+
/// Checks if the app is currently registered for startup and syncs the setting.
140+
/// Call this on app startup to ensure settings match actual state.
141+
/// </summary>
142+
public void SyncStartupSetting()
143+
{
144+
try
145+
{
146+
using var key = Registry.CurrentUser.OpenSubKey(StartupRegistryKey, writable: false);
147+
var isRegistered = key?.GetValue(AppName) is not null;
148+
149+
// Update setting to match registry state
150+
if (Settings.LaunchOnStartup != isRegistered)
151+
{
152+
Settings.LaunchOnStartup = isRegistered;
153+
SaveSettings();
154+
}
155+
}
156+
catch
157+
{
158+
// Silently fail
159+
}
160+
}
161+
162+
private class SettingsData
163+
{
164+
public bool LaunchOnStartup { get; set; }
165+
public bool StartMinimized { get; set; }
166+
public bool MinimizeToTray { get; set; } = true;
167+
public bool CloseToTray { get; set; } = true;
168+
}
169+
}

src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,11 @@
8989
<TextBlock Text="Installed"/>
9090
</StackPanel>
9191
</RadioButton>
92-
<!-- Settings tab hidden for now -->
9392
<RadioButton
9493
x:Name="SettingsTab"
9594
GroupName="MainTabs"
9695
Checked="OnTabChanged"
97-
Style="{StaticResource PillTabStyle}"
98-
Visibility="Collapsed">
96+
Style="{StaticResource PillTabStyle}">
9997
<StackPanel Orientation="Horizontal" Spacing="8">
10098
<FontIcon Glyph="&#xE713;" FontSize="14"/>
10199
<TextBlock Text="Settings"/>
@@ -241,16 +239,100 @@
241239

242240
<!-- Settings Tab Content -->
243241
<ScrollViewer x:Name="SettingsContent" Grid.Row="1" Visibility="Collapsed">
244-
<StackPanel Spacing="24" Padding="0,0,16,0">
245-
<!-- Placeholder for settings content -->
242+
<StackPanel Spacing="8" Padding="0,0,16,0">
243+
<!-- Startup Section -->
246244
<TextBlock
247-
Text="Settings"
248-
FontSize="20"
245+
Text="Startup"
246+
FontSize="14"
249247
FontWeight="SemiBold"
250-
Style="{StaticResource AppTitleStyle}"/>
248+
Margin="0,0,0,4"/>
249+
250+
<!-- Launch on startup -->
251+
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" CornerRadius="6" Padding="12,8">
252+
<Grid>
253+
<Grid.ColumnDefinitions>
254+
<ColumnDefinition Width="*"/>
255+
<ColumnDefinition Width="Auto"/>
256+
</Grid.ColumnDefinitions>
257+
<StackPanel Grid.Column="0" VerticalAlignment="Center">
258+
<TextBlock Text="Launch on startup" FontWeight="SemiBold"/>
259+
<TextBlock Text="Start Visual Studio Toolbox when Windows starts"
260+
FontSize="12"
261+
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
262+
</StackPanel>
263+
<ToggleSwitch x:Name="LaunchOnStartupToggle"
264+
Grid.Column="1"
265+
Toggled="OnLaunchOnStartupToggled"
266+
OnContent="" OffContent=""/>
267+
</Grid>
268+
</Border>
269+
270+
<!-- Start minimized -->
271+
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" CornerRadius="6" Padding="12,8">
272+
<Grid>
273+
<Grid.ColumnDefinitions>
274+
<ColumnDefinition Width="*"/>
275+
<ColumnDefinition Width="Auto"/>
276+
</Grid.ColumnDefinitions>
277+
<StackPanel Grid.Column="0" VerticalAlignment="Center">
278+
<TextBlock Text="Start minimized" FontWeight="SemiBold"/>
279+
<TextBlock Text="Start the app minimized to the system tray"
280+
FontSize="12"
281+
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
282+
</StackPanel>
283+
<ToggleSwitch x:Name="StartMinimizedToggle"
284+
Grid.Column="1"
285+
Toggled="OnStartMinimizedToggled"
286+
OnContent="" OffContent=""/>
287+
</Grid>
288+
</Border>
289+
290+
<!-- Behavior Section -->
251291
<TextBlock
252-
Text="Settings options will appear here."
253-
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
292+
Text="Behavior"
293+
FontSize="14"
294+
FontWeight="SemiBold"
295+
Margin="0,8,0,4"/>
296+
297+
<!-- Minimize to tray -->
298+
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" CornerRadius="6" Padding="12,8">
299+
<Grid>
300+
<Grid.ColumnDefinitions>
301+
<ColumnDefinition Width="*"/>
302+
<ColumnDefinition Width="Auto"/>
303+
</Grid.ColumnDefinitions>
304+
<StackPanel Grid.Column="0" VerticalAlignment="Center">
305+
<TextBlock Text="Minimize to tray" FontWeight="SemiBold"/>
306+
<TextBlock Text="Hide to system tray when minimizing"
307+
FontSize="12"
308+
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
309+
</StackPanel>
310+
<ToggleSwitch x:Name="MinimizeToTrayToggle"
311+
Grid.Column="1"
312+
Toggled="OnMinimizeToTrayToggled"
313+
OnContent="" OffContent=""/>
314+
</Grid>
315+
</Border>
316+
317+
<!-- Close to tray -->
318+
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" CornerRadius="6" Padding="12,8">
319+
<Grid>
320+
<Grid.ColumnDefinitions>
321+
<ColumnDefinition Width="*"/>
322+
<ColumnDefinition Width="Auto"/>
323+
</Grid.ColumnDefinitions>
324+
<StackPanel Grid.Column="0" VerticalAlignment="Center">
325+
<TextBlock Text="Close to tray" FontWeight="SemiBold"/>
326+
<TextBlock Text="Hide to system tray instead of closing the app"
327+
FontSize="12"
328+
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
329+
</StackPanel>
330+
<ToggleSwitch x:Name="CloseToTrayToggle"
331+
Grid.Column="1"
332+
Toggled="OnCloseToTrayToggled"
333+
OnContent="" OffContent=""/>
334+
</Grid>
335+
</Border>
254336
</StackPanel>
255337
</ScrollViewer>
256338

0 commit comments

Comments
 (0)