using System; using System.Runtime.InteropServices; namespace ClipForge { public class TrayIconService : IDisposable { [DllImport("shell32.dll", CharSet = CharSet.Unicode)] private static extern bool Shell_NotifyIcon(uint dwMessage, ref NOTIFYICONDATA lpdata); [DllImport("user32.dll")] private static extern bool DestroyMenu(IntPtr hMenu); [DllImport("user32.dll")] private static extern IntPtr CreatePopupMenu(); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool AppendMenu(IntPtr hMenu, uint uFlags, uint uIDNewItem, string lpNewItem); [DllImport("user32.dll")] private static extern int TrackPopupMenu(IntPtr hMenu, uint uFlags, int x, int y, int nReserved, IntPtr hWnd, IntPtr prcRect); [DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT lpPoint); [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); // This overload loads from a file path [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr LoadImage(IntPtr hInst, string lpszName, uint uType, int cxDesired, int cyDesired, uint fuLoad); // This overload loads a system icon by ID [DllImport("user32.dll")] private static extern IntPtr LoadImage(IntPtr hInst, IntPtr lpszName, uint uType, int cxDesired, int cyDesired, uint fuLoad); [DllImport("user32.dll")] private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, WndProcDelegate newProc); [DllImport("user32.dll")] private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll")] private static extern IntPtr GetModuleHandle(string? lpModuleName); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct NOTIFYICONDATA { public uint cbSize; public IntPtr hWnd; public uint uID; public uint uFlags; public uint uCallbackMessage; public IntPtr hIcon; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string szTip; public uint dwState; public uint dwStateMask; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string szInfo; public uint uTimeoutOrVersion; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string szInfoTitle; public uint dwInfoFlags; } [StructLayout(LayoutKind.Sequential)] private struct POINT { public int x; public int y; } private delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); private const uint NIM_ADD = 0x00000000; private const uint NIM_MODIFY = 0x00000001; private const uint NIM_DELETE = 0x00000002; private const uint NIF_MESSAGE = 0x00000001; private const uint NIF_ICON = 0x00000002; private const uint NIF_TIP = 0x00000004; private const uint WM_APP = 0x8000; private const uint WM_TRAYICON = WM_APP + 1; private const uint WM_RBUTTONUP = 0x0205; private const uint WM_LBUTTONDBLCLK = 0x0203; private const uint MF_STRING = 0x00000000; private const uint MF_SEPARATOR = 0x00000800; private const uint TPM_RETURNCMD = 0x0100; private const int IDI_APPLICATION = 32512; private const uint IMAGE_ICON = 1; private const uint LR_SHARED = 0x00008000; private const uint LR_LOADFROMFILE = 0x00000010; private const int GWLP_WNDPROC = -4; private const uint CMD_OPEN = 1001; private const uint CMD_EXIT = 1002; private NOTIFYICONDATA _iconData; private IntPtr _hwnd; private bool _added; private WndProcDelegate? _wndProc; private IntPtr _oldWndProc; private Action? _showWindow; private Action? _exitApp; public void Initialize(IntPtr hwnd, Action showWindow, Action exitApp) { _hwnd = hwnd; _showWindow = showWindow; _exitApp = exitApp; _wndProc = TrayWndProc; _oldWndProc = SetWindowLongPtr(_hwnd, GWLP_WNDPROC, _wndProc); // Try to load our custom icon from the app directory var iconPath = System.IO.Path.Combine( AppContext.BaseDirectory, "clipforge.ico"); IntPtr hIcon; if (System.IO.File.Exists(iconPath)) { hIcon = LoadImage(IntPtr.Zero, iconPath, IMAGE_ICON, 16, 16, LR_LOADFROMFILE); } else { // Fallback to generic Windows icon hIcon = LoadImage(IntPtr.Zero, new IntPtr(IDI_APPLICATION), IMAGE_ICON, 16, 16, LR_SHARED); } _iconData = new NOTIFYICONDATA { cbSize = (uint)Marshal.SizeOf(), hWnd = _hwnd, uID = 1, uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE, uCallbackMessage = WM_TRAYICON, hIcon = hIcon, szTip = "ClipForge — Recording" }; Shell_NotifyIcon(NIM_ADD, ref _iconData); _added = true; } private IntPtr TrayWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { if (msg == WM_TRAYICON) { uint mouseEvent = (uint)lParam & 0xFFFF; if (mouseEvent == WM_LBUTTONDBLCLK) _showWindow?.Invoke(); if (mouseEvent == WM_RBUTTONUP) ShowContextMenu(); } return CallWindowProc(_oldWndProc, hWnd, msg, wParam, lParam); } private void ShowContextMenu() { var hMenu = CreatePopupMenu(); AppendMenu(hMenu, MF_STRING, CMD_OPEN, "Open ClipForge"); AppendMenu(hMenu, MF_SEPARATOR, 0, ""); AppendMenu(hMenu, MF_STRING, CMD_EXIT, "Exit"); GetCursorPos(out POINT pt); SetForegroundWindow(_hwnd); int cmd = TrackPopupMenu(hMenu, TPM_RETURNCMD, pt.x, pt.y, 0, _hwnd, IntPtr.Zero); DestroyMenu(hMenu); if (cmd == CMD_OPEN) _showWindow?.Invoke(); if (cmd == CMD_EXIT) _exitApp?.Invoke(); } public void SetStatus(string status) { if (!_added) return; _iconData.szTip = $"ClipForge — {status}"; Shell_NotifyIcon(NIM_MODIFY, ref _iconData); } public void Dispose() { if (_added) { Shell_NotifyIcon(NIM_DELETE, ref _iconData); _added = false; } } } }