230 lines
8.4 KiB
C#
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;
|
|
}
|
|
}
|