129 lines
3.9 KiB
C#
129 lines
3.9 KiB
C#
using CursorLang.Agent.Interop;
|
|
using CursorLang.Core.Interop;
|
|
using CursorLang.Core.Services;
|
|
|
|
namespace CursorLang.Agent.Windows;
|
|
|
|
/// <summary>
|
|
/// The icon in the notification area: the way to the settings window and the only way
|
|
/// to quit the application.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The interop half is the same as it always was — the icon has never known anything
|
|
/// about the framework. What changed is the menu: a WPF <c>ContextMenu</c> obeyed the
|
|
/// theme and the language chosen in the settings, and a <c>TrackPopupMenuEx</c> menu is
|
|
/// drawn by Windows in the system look. The language still reaches it, because the
|
|
/// captions are ours; the theme does not, and that is the price of taking the rendering
|
|
/// stack out of the background process.
|
|
///
|
|
/// The captions are read each time the menu is raised rather than once: the language is
|
|
/// changed in the settings without a restart, and the menu is built on every click
|
|
/// anyway — a menu costs nothing to build and is asked for rarely.
|
|
/// </remarks>
|
|
internal sealed class NativeTrayIcon : IDisposable
|
|
{
|
|
/// <summary>Windows shows it under the pointer. The name of the app says enough.</summary>
|
|
private const string Tooltip = "CursorLang";
|
|
|
|
/// <summary>Distinguishes the icon among those of the same window; we have one.</summary>
|
|
private const int IconId = 1;
|
|
|
|
private const int CommandSettings = 1;
|
|
private const int CommandExit = 2;
|
|
|
|
private readonly AgentWindow _window;
|
|
private readonly ILocalizationService _localization;
|
|
|
|
private IntPtr _icon;
|
|
private bool _isInstalled;
|
|
|
|
internal NativeTrayIcon(AgentWindow window, ILocalizationService localization)
|
|
{
|
|
_window = window;
|
|
_localization = localization;
|
|
|
|
_window.AddFilter(OnMessage);
|
|
}
|
|
|
|
internal event EventHandler? OpenRequested;
|
|
|
|
internal event EventHandler? ExitRequested;
|
|
|
|
internal bool Install()
|
|
{
|
|
if (_isInstalled)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
_icon = TrayIconNative.LoadApplicationIcon();
|
|
_isInstalled = TrayIconNative.Add(_window.Handle, IconId, _icon, Tooltip);
|
|
|
|
return _isInstalled;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_isInstalled)
|
|
{
|
|
TrayIconNative.Remove(_window.Handle, IconId);
|
|
_isInstalled = false;
|
|
}
|
|
|
|
TrayIconNative.ReleaseIcon(_icon);
|
|
_icon = IntPtr.Zero;
|
|
}
|
|
|
|
private bool OnMessage(uint message, IntPtr wParam, IntPtr lParam)
|
|
{
|
|
// Explorer has restarted and taken every icon down with it
|
|
if (message == (uint)TrayIconNative.TaskbarCreatedMessage && _isInstalled)
|
|
{
|
|
TrayIconNative.Add(_window.Handle, IconId, _icon, Tooltip);
|
|
return true;
|
|
}
|
|
|
|
if (message != TrayIconNative.CallbackMessage)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
switch (TrayIconNative.NotificationOf(lParam))
|
|
{
|
|
case TrayIconNative.SelectNotification:
|
|
case TrayIconNative.KeySelectNotification:
|
|
OpenRequested?.Invoke(this, EventArgs.Empty);
|
|
return true;
|
|
|
|
case TrayIconNative.ContextMenuNotification:
|
|
ShowMenu(TrayIconNative.PointOf(wParam));
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Raises the menu of the icon where the pointer is.</summary>
|
|
internal void ShowMenu(PopupWindowNative.Point at)
|
|
{
|
|
MenuNative.Item[] items =
|
|
[
|
|
new(CommandSettings, _localization["TrayMenuSettings"]),
|
|
MenuNative.Item.Separator,
|
|
new(CommandExit, _localization["TrayMenuExit"]),
|
|
];
|
|
|
|
switch (MenuNative.Track(_window.Handle, at, items))
|
|
{
|
|
case CommandSettings:
|
|
OpenRequested?.Invoke(this, EventArgs.Empty);
|
|
break;
|
|
|
|
case CommandExit:
|
|
ExitRequested?.Invoke(this, EventArgs.Empty);
|
|
break;
|
|
}
|
|
}
|
|
}
|