using System; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; // ContextMenu, MenuItem using System.Windows.Controls.Primitives; // PlacementMode using System.Windows.Interop; namespace Bedtimer { public sealed class TrayHost : IDisposable { // === Win32 interop === [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct NOTIFYICONDATA { public int cbSize; public IntPtr hWnd; public int uID; public int uFlags; public int uCallbackMessage; public IntPtr hIcon; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string szTip; public int dwState; public int dwStateMask; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string szInfo; public int uTimeoutOrVersion; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string szInfoTitle; public int dwInfoFlags; public Guid guidItem; public IntPtr hBalloonIcon; } private const int NIM_ADD = 0x00000000; private const int NIM_MODIFY = 0x00000001; private const int NIM_DELETE = 0x00000002; private const int NIF_MESSAGE = 0x00000001; private const int NIF_ICON = 0x00000002; private const int NIF_TIP = 0x00000004; private const int WM_APP = 0x8000; private const int WM_USER = 0x0400; private const int WM_LBUTTONUP = 0x0202; private const int WM_LBUTTONDBLCLK = 0x0203; private const int WM_RBUTTONUP = 0x0205; [DllImport("shell32.dll", CharSet = CharSet.Unicode)] private static extern bool Shell_NotifyIcon(int dwMessage, ref NOTIFYICONDATA lpdata); [DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT lpPoint); [DllImport("user32.dll")] private static extern IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName); private static readonly IntPtr IDI_INFORMATION = (IntPtr)0x7F04; // stock info icon [StructLayout(LayoutKind.Sequential)] private struct POINT { public int X; public int Y; } // === Fields === private HwndSource? _src; private IntPtr _hWnd = IntPtr.Zero; private IntPtr _hIcon = IntPtr.Zero; private int _iconId = 1; private readonly int _callbackMsg = WM_APP + 1; private ContextMenu? _menu; private OverlayManager _overlay = null!; public void Init() { // Hidden message window for tray callbacks var param = new HwndSourceParameters("Bedtimer.TrayHostWindow") { Width = 0, Height = 0, WindowStyle = unchecked((int)0x80000000), // WS_POPUP UsesPerPixelOpacity = false }; _src = new HwndSource(param); _src.AddHook(WndProc); _hWnd = _src.Handle; // Icon (stock system icon to avoid resources) _hIcon = LoadIcon(IntPtr.Zero, IDI_INFORMATION); // Add tray icon var nid = new NOTIFYICONDATA { cbSize = Marshal.SizeOf(), hWnd = _hWnd, uID = _iconId, uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP, uCallbackMessage = _callbackMsg, hIcon = _hIcon, szTip = "Bedtimer" }; if (!Shell_NotifyIcon(NIM_ADD, ref nid)) throw new Win32Exception("Shell_NotifyIcon(NIM_ADD) failed."); // Build WPF context menu _menu = BuildMenu(); // Start overlay manager _overlay = new OverlayManager(); _overlay.Start(); } private ContextMenu BuildMenu() { var menu = new ContextMenu(); var open = new MenuItem { Header = "Open Settings..." }; open.Click += (_, __) => ShowSettings(); menu.Items.Add(open); var autostart = new MenuItem { Header = "Start with Windows", IsCheckable = true, IsChecked = AutoStart.IsEnabled() }; autostart.Click += (_, __) => { var next = !AutoStart.IsEnabled(); AutoStart.SetEnabled(next); // may UAC prompt autostart.IsChecked = AutoStart.IsEnabled(); // refresh state after hop }; menu.Items.Add(autostart); menu.Items.Add(new Separator()); var exit = new MenuItem { Header = "Exit" }; exit.Click += (_, __) => Application.Current.Shutdown(); menu.Items.Add(exit); return menu; } private void ShowSettings() { // Bring up settings window (single instance) foreach (Window w in Application.Current.Windows) if (w is MainWindow mw) { mw.Activate(); return; } var win = new MainWindow(); win.Show(); win.Activate(); } // Show WPF ContextMenu at the current cursor position private void ShowMenuAtCursor() { if (_menu == null) return; if (GetCursorPos(out var pt)) { _menu.Placement = PlacementMode.AbsolutePoint; _menu.HorizontalOffset = pt.X; _menu.VerticalOffset = pt.Y; _menu.IsOpen = true; _menu.Focus(); } } private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == _callbackMsg) { int code = lParam.ToInt32(); switch (code) { case WM_LBUTTONUP: ShowSettings(); handled = true; break; case WM_LBUTTONDBLCLK: ShowSettings(); handled = true; break; case WM_RBUTTONUP: ShowMenuAtCursor(); handled = true; break; } } return IntPtr.Zero; } public void Dispose() { try { var nid = new NOTIFYICONDATA { cbSize = Marshal.SizeOf(), hWnd = _hWnd, uID = _iconId }; Shell_NotifyIcon(NIM_DELETE, ref nid); } catch { /* ignore */ } _overlay?.Dispose(); _src?.RemoveHook(WndProc); _src?.Dispose(); _src = null; } } }