Files
Bedtimer/TrayHost.cs
2025-10-26 10:40:21 -05:00

208 lines
6.9 KiB
C#

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<NOTIFYICONDATA>(),
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<NOTIFYICONDATA>(),
hWnd = _hWnd,
uID = _iconId
};
Shell_NotifyIcon(NIM_DELETE, ref nid);
}
catch { /* ignore */ }
_overlay?.Dispose();
_src?.RemoveHook(WndProc);
_src?.Dispose();
_src = null;
}
}
}