0b7c20a1fa
* GUI: hide-to-tray on X button; tray persists until explicit Exit The minimize-to-tray behavior already worked, but clicking the X button killed the GUI process and took the tray with it. That made "tray when the GUI window is closed" a UX dead end - the only way to get the tray was to leave the window minimized. Now: - X button / Alt+F4 -> hide window, tray stays alive - Tray double-click -> reopens window - File -> Exit (or tray's Exit menu) -> truly quits the process Wired by adding a RealExitRequested event on MainViewModel that the window subscribes to (so File -> Exit sets the ExitForReal flag before calling Shutdown), and a parallel onExit callback on TrayIcon for the tray menu's Exit item. The Closing handler checks ExitForReal: if false (X / Alt+F4) it cancels the close and hides; if true, it disposes the tray and lets the close proceed. Auto-start at login is still TBD - if you want the tray to be there without manually launching the GUI after a reboot, that's a separate Task Scheduler entry. Skipping for now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add File -> Minimize to tray toggle (default on) Adds a checkable MenuItem so the user can opt out of the hide-to-tray behavior. Persisted per-user to %APPDATA%\WebhookServer\gui.json so the choice survives restarts. When ticked (default): X / Alt+F4 / minimize hide to tray, GUI process keeps running, tray icon persists. When unticked: X actually closes the app, minimize is a regular Windows minimize. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix endpoint-row context menu: bindings via PlacementTarget.Tag The ContextMenu lived in its own popup visual tree, so the menu items' RelativeSource={RelativeSource AncestorType=Window} couldn't find the Window and the bindings silently failed - none of Edit / Copy URL / Toggle / Delete actually fired their commands. Standard WPF workaround: park MainViewModel on each DataGridRow's Tag (still in the Window's visual tree, so the row Setter binding resolves) and reach it from the menu items via PlacementTarget.Tag. The toggle command parameter likewise comes from PlacementTarget.DataContext (the EndpointConfig the row represents). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * v0.1.3 --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
89 lines
2.7 KiB
C#
89 lines
2.7 KiB
C#
using System.Drawing;
|
|
using System.Runtime.Versioning;
|
|
using System.Windows;
|
|
using System.Windows.Forms;
|
|
|
|
namespace WebhookServer.Gui.Services;
|
|
|
|
/// <summary>
|
|
/// Minimal system tray icon using Windows Forms NotifyIcon. Owns a context menu
|
|
/// (Open / Restart service / Exit) and toggles the main window visibility on
|
|
/// double-click. Hide-to-tray on minimize is wired in MainWindow.xaml.cs.
|
|
/// </summary>
|
|
[SupportedOSPlatform("windows")]
|
|
public sealed class TrayIcon : IDisposable
|
|
{
|
|
private readonly NotifyIcon _icon;
|
|
private readonly Func<Window?> _resolveMainWindow;
|
|
private readonly Func<Task> _restartServiceAsync;
|
|
private readonly Action _onExit;
|
|
|
|
public TrayIcon(Func<Window?> resolveMainWindow, Func<Task> restartServiceAsync, Action onExit)
|
|
{
|
|
_resolveMainWindow = resolveMainWindow;
|
|
_restartServiceAsync = restartServiceAsync;
|
|
_onExit = onExit;
|
|
|
|
_icon = new NotifyIcon
|
|
{
|
|
Icon = LoadEmbeddedIcon(),
|
|
Text = "Webhook Server",
|
|
Visible = true,
|
|
};
|
|
_icon.DoubleClick += (_, _) => ShowMainWindow();
|
|
_icon.ContextMenuStrip = BuildMenu();
|
|
}
|
|
|
|
private ContextMenuStrip BuildMenu()
|
|
{
|
|
var menu = new ContextMenuStrip();
|
|
menu.Items.Add("&Open Webhook Server", null, (_, _) => ShowMainWindow());
|
|
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, (_, _) => _onExit());
|
|
return menu;
|
|
}
|
|
|
|
private void ShowMainWindow()
|
|
{
|
|
var w = _resolveMainWindow();
|
|
if (w is null) return;
|
|
if (w.WindowState == WindowState.Minimized) w.WindowState = WindowState.Normal;
|
|
w.Show();
|
|
w.Activate();
|
|
w.Topmost = true;
|
|
w.Topmost = false;
|
|
}
|
|
|
|
private static Icon LoadEmbeddedIcon()
|
|
{
|
|
// Pulled from the WPF Resource items in the csproj via the application
|
|
// pack URI. Falling back to SystemIcons keeps the tray usable if the
|
|
// resource is somehow missing.
|
|
try
|
|
{
|
|
var uri = new Uri("pack://application:,,,/webhook-server.ico", UriKind.Absolute);
|
|
using var stream = Application.GetResourceStream(uri).Stream;
|
|
return new Icon(stream);
|
|
}
|
|
catch
|
|
{
|
|
return SystemIcons.Application;
|
|
}
|
|
}
|
|
|
|
public void ShowBalloon(string title, string message)
|
|
{
|
|
_icon.BalloonTipTitle = title;
|
|
_icon.BalloonTipText = message;
|
|
_icon.ShowBalloonTip(3000);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_icon.Visible = false;
|
|
_icon.Dispose();
|
|
}
|
|
}
|