using CursorLang.Agent.Interop; using CursorLang.Core.Interop; using CursorLang.Core.Services; namespace CursorLang.Agent.Windows; /// /// The icon in the notification area: the way to the settings window and the only way /// to quit the application. /// /// /// 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 ContextMenu obeyed the /// theme and the language chosen in the settings, and a TrackPopupMenuEx 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. /// internal sealed class NativeTrayIcon : IDisposable { /// Windows shows it under the pointer. The name of the app says enough. private const string Tooltip = "CursorLang"; /// Distinguishes the icon among those of the same window; we have one. 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; } } /// Raises the menu of the icon where the pointer is. 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; } } }