initial
This commit is contained in:
207
TrayHost.cs
Normal file
207
TrayHost.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user