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]