99 lines
3.7 KiB
C#
99 lines
3.7 KiB
C#
using System.Runtime.InteropServices;
|
|
using CursorLang.Core.Interop;
|
|
|
|
namespace CursorLang.Agent.Interop;
|
|
|
|
/// <summary>
|
|
/// The system context menu — the one the tray icon raises.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A menu built this way is drawn by Windows, so the theme and the language chosen in
|
|
/// the application no longer reach it. That is the accepted price of leaving WPF: a WPF
|
|
/// <c>ContextMenu</c> costs the whole rendering stack in the background process.
|
|
/// </remarks>
|
|
internal static class MenuNative
|
|
{
|
|
private const uint MF_STRING = 0x00000000;
|
|
private const uint MF_SEPARATOR = 0x00000800;
|
|
private const uint MF_GRAYED = 0x00000001;
|
|
|
|
private const uint TPM_LEFTALIGN = 0x0000;
|
|
private const uint TPM_RIGHTBUTTON = 0x0002;
|
|
private const uint TPM_RETURNCMD = 0x0100;
|
|
|
|
/// <summary>An item of the menu being built.</summary>
|
|
/// <param name="Id">What <see cref="Track"/> gives back when the item is chosen.</param>
|
|
/// <param name="Caption">The text, or <c>null</c> for a separator.</param>
|
|
/// <param name="IsEnabled">A greyed item is shown but cannot be chosen.</param>
|
|
internal readonly record struct Item(int Id, string? Caption, bool IsEnabled = true)
|
|
{
|
|
internal static Item Separator => new(0, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raises the menu at a screen point and returns the identifier of the chosen item,
|
|
/// or zero when the user dismissed it.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <c>TPM_RETURNCMD</c> means the answer comes back from the call itself instead of
|
|
/// as a <c>WM_COMMAND</c> later, which keeps the whole menu in one place. The call
|
|
/// does not return until the user is done with the menu — that is how a modal menu
|
|
/// works, and the message loop keeps running inside it.
|
|
///
|
|
/// The window is brought to the foreground first and poked with an empty message
|
|
/// afterwards: without the first the menu never closes on a click elsewhere, and
|
|
/// without the second it stays on screen after the choice is made. Both are
|
|
/// long-standing quirks of a menu owned by a window the user cannot see.
|
|
/// </remarks>
|
|
internal static int Track(IntPtr owner, PopupWindowNative.Point at, IReadOnlyList<Item> items)
|
|
{
|
|
IntPtr menu = CreatePopupMenu();
|
|
if (menu == IntPtr.Zero)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
try
|
|
{
|
|
foreach (Item item in items)
|
|
{
|
|
if (item.Caption is null)
|
|
{
|
|
AppendMenu(menu, MF_SEPARATOR, IntPtr.Zero, null);
|
|
continue;
|
|
}
|
|
|
|
uint flags = MF_STRING | (item.IsEnabled ? 0 : MF_GRAYED);
|
|
AppendMenu(menu, flags, new IntPtr(item.Id), item.Caption);
|
|
}
|
|
|
|
TrayIconNative.BringToForeground(owner);
|
|
|
|
int chosen = TrackPopupMenuEx(
|
|
menu, TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
|
|
at.X, at.Y, owner, IntPtr.Zero);
|
|
|
|
WindowNative.PostMessage(owner, WindowNative.WM_NULL, IntPtr.Zero, IntPtr.Zero);
|
|
|
|
return chosen;
|
|
}
|
|
finally
|
|
{
|
|
DestroyMenu(menu);
|
|
}
|
|
}
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern IntPtr CreatePopupMenu();
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool DestroyMenu(IntPtr hMenu);
|
|
|
|
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "AppendMenuW")]
|
|
private static extern bool AppendMenu(IntPtr hMenu, uint uFlags, IntPtr uIDNewItem, string? lpNewItem);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern int TrackPopupMenuEx(IntPtr hMenu, uint uFlags, int x, int y,
|
|
IntPtr hwnd, IntPtr lptpm);
|
|
}
|