diff --git a/src/WebhookServer.Gui/MainWindow.xaml.cs b/src/WebhookServer.Gui/MainWindow.xaml.cs index 9f387ff..244e345 100644 --- a/src/WebhookServer.Gui/MainWindow.xaml.cs +++ b/src/WebhookServer.Gui/MainWindow.xaml.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -11,19 +12,48 @@ public partial class MainWindow : Window private readonly TrayIcon _tray; private readonly MainViewModel _vm; + /// + /// Set to true when the user has explicitly asked to quit (File -> Exit or + /// Tray -> Exit). The OnClosing handler reads this to decide whether to + /// actually let the window close or hide it to the tray. + /// + public bool ExitForReal { get; set; } + public MainWindow() { InitializeComponent(); _vm = new MainViewModel(new AdminPipeClient()); DataContext = _vm; + _vm.RealExitRequested += OnRealExitRequested; _tray = new TrayIcon( resolveMainWindow: () => Application.Current.MainWindow, - restartServiceAsync: async () => await new AdminPipeClient().RestartListenerAsync()); + restartServiceAsync: async () => await new AdminPipeClient().RestartListenerAsync(), + onExit: OnRealExitRequested); Loaded += async (_, _) => await _vm.RefreshCommand.ExecuteAsync(null); StateChanged += OnStateChanged; - Closed += (_, _) => _tray.Dispose(); + Closing += OnClosing; + } + + private void OnClosing(object? sender, CancelEventArgs e) + { + if (ExitForReal) + { + _tray.Dispose(); + return; + } + // Treat the X button / Alt+F4 like a minimize: hide to tray, keep the + // process alive so the tray icon persists. + e.Cancel = true; + Hide(); + ShowInTaskbar = false; + } + + private void OnRealExitRequested() + { + ExitForReal = true; + Application.Current.Shutdown(); } private void OnStateChanged(object? sender, EventArgs e) diff --git a/src/WebhookServer.Gui/Services/TrayIcon.cs b/src/WebhookServer.Gui/Services/TrayIcon.cs index 7cedfa5..4eb507c 100644 --- a/src/WebhookServer.Gui/Services/TrayIcon.cs +++ b/src/WebhookServer.Gui/Services/TrayIcon.cs @@ -16,11 +16,13 @@ public sealed class TrayIcon : IDisposable private readonly NotifyIcon _icon; private readonly Func _resolveMainWindow; private readonly Func _restartServiceAsync; + private readonly Action _onExit; - public TrayIcon(Func resolveMainWindow, Func restartServiceAsync) + public TrayIcon(Func resolveMainWindow, Func restartServiceAsync, Action onExit) { _resolveMainWindow = resolveMainWindow; _restartServiceAsync = restartServiceAsync; + _onExit = onExit; _icon = new NotifyIcon { @@ -39,7 +41,7 @@ public sealed class TrayIcon : IDisposable menu.Items.Add(new ToolStripSeparator()); menu.Items.Add("&Restart service", null, async (_, _) => await _restartServiceAsync().ConfigureAwait(false)); menu.Items.Add(new ToolStripSeparator()); - menu.Items.Add("E&xit", null, (_, _) => Application.Current.Shutdown()); + menu.Items.Add("E&xit", null, (_, _) => _onExit()); return menu; } diff --git a/src/WebhookServer.Gui/ViewModels/MainViewModel.cs b/src/WebhookServer.Gui/ViewModels/MainViewModel.cs index 5e944b6..e2bed34 100644 --- a/src/WebhookServer.Gui/ViewModels/MainViewModel.cs +++ b/src/WebhookServer.Gui/ViewModels/MainViewModel.cs @@ -286,10 +286,14 @@ public sealed partial class MainViewModel : ObservableObject } } + /// Raised when the user picks File -> Exit. MainWindow flips its + /// ExitForReal flag and shuts down, bypassing the X-hides-to-tray logic. + public event Action? RealExitRequested; + [RelayCommand] private void Exit() { - Application.Current.Shutdown(); + RealExitRequested?.Invoke(); } [RelayCommand]