diff --git a/src/CodingWithCalvin.VSToolbox/App.xaml b/src/CodingWithCalvin.VSToolbox/App.xaml index 52d1542..27e1ce2 100644 --- a/src/CodingWithCalvin.VSToolbox/App.xaml +++ b/src/CodingWithCalvin.VSToolbox/App.xaml @@ -14,6 +14,12 @@ + + + + + + diff --git a/src/CodingWithCalvin.VSToolbox/App.xaml.cs b/src/CodingWithCalvin.VSToolbox/App.xaml.cs index 50fab76..5e792b7 100644 --- a/src/CodingWithCalvin.VSToolbox/App.xaml.cs +++ b/src/CodingWithCalvin.VSToolbox/App.xaml.cs @@ -65,8 +65,10 @@ public partial class App : Application private Window? _window; private AppWindow? _appWindow; private TrayIconService? _trayIconService; + private SettingsService? _settingsService; public Window? MainWindow => _window; + public SettingsService Settings => _settingsService ??= new SettingsService(); public App() { @@ -122,7 +124,11 @@ protected override void OnLaunched(LaunchActivatedEventArgs e) // Set window size and position to bottom-right PositionWindowBottomRight(540, 600); - _window.Activate(); + // Only show window if not starting minimized + if (!Settings.StartMinimized) + { + _window.Activate(); + } } private void ConfigureCustomTitleBar() @@ -178,11 +184,21 @@ private void PositionWindowBottomRight(int width, int height) private void OnAppWindowClosing(AppWindow sender, AppWindowClosingEventArgs args) { - // Prevent window from closing - hide it instead - args.Cancel = true; + if (Settings.CloseToTray) + { + // Prevent window from closing - hide it instead + args.Cancel = true; - // Hide the window - _appWindow?.Hide(); + // Hide the window + _appWindow?.Hide(); + } + else + { + // Actually close the app + _trayIconService?.Dispose(); + _mutex?.ReleaseMutex(); + _mutex?.Dispose(); + } } private void OnNavigationFailed(object sender, NavigationFailedEventArgs e) diff --git a/src/CodingWithCalvin.VSToolbox/Services/SettingsService.cs b/src/CodingWithCalvin.VSToolbox/Services/SettingsService.cs new file mode 100644 index 0000000..0e473f7 --- /dev/null +++ b/src/CodingWithCalvin.VSToolbox/Services/SettingsService.cs @@ -0,0 +1,169 @@ +using System.Text.Json; +using Microsoft.Win32; + +namespace CodingWithCalvin.VSToolbox.Services; + +public class SettingsService +{ + private const string StartupRegistryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; + private const string AppName = "VSToolbox"; + private const string SettingsFileName = "settings.json"; + + private static readonly string SettingsFilePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "CodingWithCalvin.VSToolbox", + SettingsFileName); + + private static SettingsData? _settings; + private static readonly object _lock = new(); + + private static SettingsData Settings + { + get + { + if (_settings is null) + { + lock (_lock) + { + _settings ??= LoadSettings(); + } + } + return _settings; + } + } + + public bool LaunchOnStartup + { + get => Settings.LaunchOnStartup; + set + { + Settings.LaunchOnStartup = value; + SaveSettings(); + UpdateStartupRegistration(value); + } + } + + public bool StartMinimized + { + get => Settings.StartMinimized; + set + { + Settings.StartMinimized = value; + SaveSettings(); + } + } + + public bool MinimizeToTray + { + get => Settings.MinimizeToTray; + set + { + Settings.MinimizeToTray = value; + SaveSettings(); + } + } + + public bool CloseToTray + { + get => Settings.CloseToTray; + set + { + Settings.CloseToTray = value; + SaveSettings(); + } + } + + private static SettingsData LoadSettings() + { + try + { + if (File.Exists(SettingsFilePath)) + { + var json = File.ReadAllText(SettingsFilePath); + return JsonSerializer.Deserialize(json) ?? new SettingsData(); + } + } + catch + { + // If we can't load settings, return defaults + } + return new SettingsData(); + } + + private static void SaveSettings() + { + try + { + var directory = Path.GetDirectoryName(SettingsFilePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + var json = JsonSerializer.Serialize(_settings, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(SettingsFilePath, json); + } + catch + { + // Silently fail if we can't save settings + } + } + + private static void UpdateStartupRegistration(bool enable) + { + try + { + using var key = Registry.CurrentUser.OpenSubKey(StartupRegistryKey, writable: true); + if (key is null) return; + + if (enable) + { + var exePath = Environment.ProcessPath; + if (!string.IsNullOrEmpty(exePath)) + { + key.SetValue(AppName, $"\"{exePath}\""); + } + } + else + { + key.DeleteValue(AppName, throwOnMissingValue: false); + } + } + catch + { + // Silently fail if we can't access the registry + } + } + + /// + /// Checks if the app is currently registered for startup and syncs the setting. + /// Call this on app startup to ensure settings match actual state. + /// + public void SyncStartupSetting() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey(StartupRegistryKey, writable: false); + var isRegistered = key?.GetValue(AppName) is not null; + + // Update setting to match registry state + if (Settings.LaunchOnStartup != isRegistered) + { + Settings.LaunchOnStartup = isRegistered; + SaveSettings(); + } + } + catch + { + // Silently fail + } + } + + private class SettingsData + { + public bool LaunchOnStartup { get; set; } + public bool StartMinimized { get; set; } + public bool MinimizeToTray { get; set; } = true; + public bool CloseToTray { get; set; } = true; + } +} diff --git a/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml b/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml index 73ce610..82e8917 100644 --- a/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml +++ b/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml @@ -89,13 +89,11 @@ - + Style="{StaticResource PillTabStyle}"> @@ -241,16 +239,100 @@ - - + + + Margin="0,0,0,4"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Text="Behavior" + FontSize="14" + FontWeight="SemiBold" + Margin="0,8,0,4"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml.cs b/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml.cs index 528ce20..44b632a 100644 --- a/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml.cs +++ b/src/CodingWithCalvin.VSToolbox/Views/MainPage.xaml.cs @@ -16,14 +16,34 @@ public sealed partial class MainPage : Page private static readonly SolidColorBrush TransparentBrush = new(Colors.Transparent); private static readonly SolidColorBrush HoverBackgroundBrush = new(Color.FromArgb(20, 138, 43, 226)); + private readonly SettingsService _settingsService = new(); + private bool _isInitializingSettings; + public MainPage() { InitializeComponent(); ViewModel = new MainViewModel(); + InitializeSettings(); } public MainViewModel ViewModel { get; } + private void InitializeSettings() + { + _isInitializingSettings = true; + + // Sync startup setting with registry state + _settingsService.SyncStartupSetting(); + + // Load current settings into toggles + LaunchOnStartupToggle.IsOn = _settingsService.LaunchOnStartup; + StartMinimizedToggle.IsOn = _settingsService.StartMinimized; + MinimizeToTrayToggle.IsOn = _settingsService.MinimizeToTray; + CloseToTrayToggle.IsOn = _settingsService.CloseToTray; + + _isInitializingSettings = false; + } + private async void OnPageLoaded(object sender, RoutedEventArgs e) { // Set the title bar drag region @@ -188,13 +208,18 @@ private void OnTabChanged(object sender, RoutedEventArgs e) private void OnMinimizeClick(object sender, RoutedEventArgs e) { - var window = App.Current as App; var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(((App)Application.Current).MainWindow); var windowId = Win32Interop.GetWindowIdFromWindow(hwnd); var appWindow = AppWindow.GetFromWindowId(windowId); - if (appWindow.Presenter is OverlappedPresenter presenter) + if (_settingsService.MinimizeToTray) { + // Hide to system tray + appWindow.Hide(); + } + else if (appWindow.Presenter is OverlappedPresenter presenter) + { + // Normal minimize presenter.Minimize(); } } @@ -205,7 +230,39 @@ private void OnCloseClick(object sender, RoutedEventArgs e) var windowId = Win32Interop.GetWindowIdFromWindow(hwnd); var appWindow = AppWindow.GetFromWindowId(windowId); - // This will trigger the Closing event which hides instead of closes - appWindow.Hide(); + if (_settingsService.CloseToTray) + { + // Hide to system tray + appWindow.Hide(); + } + else + { + // Actually close the app + Application.Current.Exit(); + } + } + + private void OnLaunchOnStartupToggled(object sender, RoutedEventArgs e) + { + if (_isInitializingSettings) return; + _settingsService.LaunchOnStartup = LaunchOnStartupToggle.IsOn; + } + + private void OnStartMinimizedToggled(object sender, RoutedEventArgs e) + { + if (_isInitializingSettings) return; + _settingsService.StartMinimized = StartMinimizedToggle.IsOn; + } + + private void OnMinimizeToTrayToggled(object sender, RoutedEventArgs e) + { + if (_isInitializingSettings) return; + _settingsService.MinimizeToTray = MinimizeToTrayToggle.IsOn; + } + + private void OnCloseToTrayToggled(object sender, RoutedEventArgs e) + { + if (_isInitializingSettings) return; + _settingsService.CloseToTray = CloseToTrayToggle.IsOn; } }