using System.Runtime.InteropServices; namespace CursorLang.Interop; /// /// Win32 API for the notification area: the icon itself, the messages it sends /// and the icon image taken from the executable. /// /// /// The icon is asked for at version 4 of the protocol. It is the only version that /// reports a request for the context menu as such — by the keyboard as well as by /// the right button — and passes the point of the click along with it. The /// notification then arrives in the low word of lParam, and the point in /// wParam, which is the opposite of the earlier versions. /// internal static class TrayIconNative { /// The message the icon sends to its window. WM_APP is free for the app. internal const int CallbackMessage = 0x8000 + 1; /// The user chose the icon: a click of the left button or Enter on it. internal const int SelectNotification = 0x0400; /// The same by the space bar — Windows tells the two apart. internal const int KeySelectNotification = 0x0403; /// The context menu is asked for: the right button or the menu key. internal const int ContextMenuNotification = 0x007B; private const int NIM_ADD = 0x00000000; private const int NIM_MODIFY = 0x00000001; private const int NIM_DELETE = 0x00000002; private const int NIM_SETVERSION = 0x00000004; private const uint NIF_MESSAGE = 0x00000001; private const uint NIF_ICON = 0x00000002; private const uint NIF_TIP = 0x00000004; private const uint NIF_SHOWTIP = 0x00000080; private const uint NotifyIconVersion4 = 4; private const uint IMAGE_ICON = 1; private const uint LR_DEFAULTCOLOR = 0x00000000; private const int SM_CXSMICON = 49; private const int SM_CYSMICON = 50; /// The resource the .NET build puts the application icon under. private const int ApplicationIconResource = 32512; /// /// Explorer says it has restarted this way. The icons of every application are /// gone by then and have to be put back. /// internal static int TaskbarCreatedMessage { get; } = RegisterWindowMessage("TaskbarCreated"); /// Puts the icon into the notification area. internal static bool Add(IntPtr window, int id, IntPtr icon, string tooltip) { NotifyIconData data = Describe(window, id, icon, tooltip); if (!Shell_NotifyIcon(NIM_ADD, ref data)) { return false; } // The version is asked for after the icon is added and applies to it alone data.uVersion = NotifyIconVersion4; Shell_NotifyIcon(NIM_SETVERSION, ref data); return true; } /// Replaces the image and the tooltip of an icon already there. internal static bool Modify(IntPtr window, int id, IntPtr icon, string tooltip) { NotifyIconData data = Describe(window, id, icon, tooltip); return Shell_NotifyIcon(NIM_MODIFY, ref data); } /// Takes the icon away. A forgotten icon stays in the tray until hovered. internal static void Remove(IntPtr window, int id) { var data = new NotifyIconData { cbSize = Marshal.SizeOf(), hWnd = window, uID = (uint)id, }; Shell_NotifyIcon(NIM_DELETE, ref data); } /// /// The icon of the application at the size the tray asks for. /// /// /// The image comes from the executable itself, so the tray shows what the user /// sees in Explorer. The build puts the icon under the standard resource; should /// it end up elsewhere, the first icon of the file is taken, and failing that — /// the icon Windows gives to an application without one. An icon is needed /// either way: without it the tray shows an empty spot. /// internal static IntPtr LoadApplicationIcon() { int width = GetSystemMetrics(SM_CXSMICON); int height = GetSystemMetrics(SM_CYSMICON); IntPtr icon = LoadImage( GetModuleHandle(null), ApplicationIconResource, IMAGE_ICON, width, height, LR_DEFAULTCOLOR); if (icon == IntPtr.Zero && Environment.ProcessPath is { Length: > 0 } path) { icon = ExtractIconEx(path, 0, out IntPtr large, out IntPtr small, 1) > 0 ? small : IntPtr.Zero; if (large != IntPtr.Zero) { DestroyIcon(large); } } return icon != IntPtr.Zero ? icon : LoadIcon(IntPtr.Zero, ApplicationIconResource); } /// Releases an icon loaded by . internal static void ReleaseIcon(IntPtr icon) { if (icon != IntPtr.Zero) { DestroyIcon(icon); } } /// The notification the icon has sent: it sits in the low word of lParam. internal static int NotificationOf(IntPtr lParam) => (int)(lParam.ToInt64() & 0xFFFF); /// /// Brings the window to the foreground. /// /// /// Windows takes a menu down when its owner window loses the foreground. A tray /// icon belongs to a window that is never shown, so the foreground has to be /// asked for by hand — otherwise the menu stays on screen after the user has /// clicked past it. /// internal static void BringToForeground(IntPtr window) => SetForegroundWindow(window); private static NotifyIconData Describe(IntPtr window, int id, IntPtr icon, string tooltip) => new() { cbSize = Marshal.SizeOf(), hWnd = window, uID = (uint)id, uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_SHOWTIP, uCallbackMessage = CallbackMessage, hIcon = icon, szTip = tooltip, }; [DllImport("shell32.dll", CharSet = CharSet.Unicode)] private static extern bool Shell_NotifyIcon(int dwMessage, ref NotifyIconData lpData); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int RegisterWindowMessage(string lpString); [DllImport("user32.dll")] private static extern IntPtr LoadImage(IntPtr hInst, IntPtr name, uint type, int cx, int cy, uint fuLoad); private static IntPtr LoadImage(IntPtr hInst, int resource, uint type, int cx, int cy, uint fuLoad) => LoadImage(hInst, new IntPtr(resource), type, cx, cy, fuLoad); [DllImport("user32.dll")] private static extern IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName); private static IntPtr LoadIcon(IntPtr hInstance, int resource) => LoadIcon(hInstance, new IntPtr(resource)); [DllImport("shell32.dll", CharSet = CharSet.Unicode)] private static extern int ExtractIconEx(string lpszFile, int nIconIndex, out IntPtr phiconLarge, out IntPtr phiconSmall, int nIcons); [DllImport("user32.dll")] private static extern bool DestroyIcon(IntPtr hIcon); [DllImport("user32.dll")] private static extern int GetSystemMetrics(int nIndex); [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr GetModuleHandle(string? lpModuleName); /// /// NOTIFYICONDATAW. The whole structure is described even though only its first /// half is used: Windows reads its size and refuses one it does not know. /// [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct NotifyIconData { public int cbSize; public IntPtr hWnd; public uint uID; public uint uFlags; public int 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; /// A timeout in the older versions and the protocol version here. public uint uVersion; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string szInfoTitle; public uint dwInfoFlags; public Guid guidItem; public IntPtr hBalloonIcon; } }