Files
cursor-lang/CursorLang.Agent/Interop/TrayIconNative.cs
T
alex 55ac8e6556 lightweight variant (#1)
Reviewed-on: #1
Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
2026-08-12 13:37:30 +00:00

230 lines
8.4 KiB
C#

using System.Runtime.InteropServices;
using CursorLang.Core.Interop;
namespace CursorLang.Agent.Interop;
/// <summary>
/// Win32 API for the notification area: the icon itself, the messages it sends
/// and the icon image taken from the executable.
/// </summary>
/// <remarks>
/// 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 <c>lParam</c>, and the point in
/// <c>wParam</c>, which is the opposite of the earlier versions.
/// </remarks>
internal static class TrayIconNative
{
/// <summary>The message the icon sends to its window. WM_APP is free for the app.</summary>
internal const int CallbackMessage = 0x8000 + 1;
/// <summary>The user chose the icon: a click of the left button or Enter on it.</summary>
internal const int SelectNotification = 0x0400;
/// <summary>The same by the space bar — Windows tells the two apart.</summary>
internal const int KeySelectNotification = 0x0403;
/// <summary>The context menu is asked for: the right button or the menu key.</summary>
internal const int ContextMenuNotification = 0x007B;
private const int NIM_ADD = 0x00000000;
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;
/// <summary>The resource the .NET build puts the application icon under.</summary>
private const int ApplicationIconResource = 32512;
/// <summary>
/// Explorer says it has restarted this way. The icons of every application are
/// gone by then and have to be put back.
/// </summary>
internal static int TaskbarCreatedMessage { get; } = RegisterWindowMessage("TaskbarCreated");
/// <summary>Puts the icon into the notification area.</summary>
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;
}
/// <summary>Takes the icon away. A forgotten icon stays in the tray until hovered.</summary>
internal static void Remove(IntPtr window, int id)
{
var data = new NotifyIconData
{
cbSize = Marshal.SizeOf<NotifyIconData>(),
hWnd = window,
uID = (uint)id,
};
Shell_NotifyIcon(NIM_DELETE, ref data);
}
/// <summary>
/// The icon of the application at the size the tray asks for.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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);
}
/// <summary>Releases an icon loaded by <see cref="LoadApplicationIcon"/>.</summary>
internal static void ReleaseIcon(IntPtr icon)
{
if (icon != IntPtr.Zero)
{
DestroyIcon(icon);
}
}
/// <summary>The notification the icon has sent: it sits in the low word of lParam.</summary>
internal static int NotificationOf(IntPtr lParam) => (int)(lParam.ToInt64() & 0xFFFF);
/// <summary>
/// The point of the click. Version 4 of the protocol reports it in screen pixels
/// in <c>wParam</c> — exactly what <c>TrackPopupMenuEx</c> expects.
/// </summary>
internal static PopupWindowNative.Point PointOf(IntPtr wParam) => new()
{
X = (short)(wParam.ToInt64() & 0xFFFF),
Y = (short)((wParam.ToInt64() >> 16) & 0xFFFF),
};
/// <summary>
/// Brings the window to the foreground.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
internal static void BringToForeground(IntPtr window) => SetForegroundWindow(window);
private static NotifyIconData Describe(IntPtr window, int id, IntPtr icon, string tooltip) => new()
{
cbSize = Marshal.SizeOf<NotifyIconData>(),
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);
/// <summary>
/// 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.
/// </summary>
[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;
/// <summary>A timeout in the older versions and the protocol version here.</summary>
public uint uVersion;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string szInfoTitle;
public uint dwInfoFlags;
public Guid guidItem;
public IntPtr hBalloonIcon;
}
}