208 lines
6.9 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|