182 lines
5.4 KiB
C#
182 lines
5.4 KiB
C#
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Data;
|
|
using System.Windows.Interop;
|
|
using CursorLang.Interop;
|
|
|
|
namespace CursorLang.Services;
|
|
|
|
/// <summary>
|
|
/// The icon in the notification area: a way to bring the settings window back and
|
|
/// the only way to quit the application.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The icon belongs to a window rather than to a process, and the application has no
|
|
/// window that is always there — the settings window is hidden most of the time.
|
|
/// So a window of its own is created here and never shown: it exists to receive the
|
|
/// messages of the icon.
|
|
///
|
|
/// The menu is a WPF one rather than a system one on purpose: that way it obeys the
|
|
/// theme and the language chosen in the settings, like the rest of the interface.
|
|
/// </remarks>
|
|
public sealed class TrayIcon : ITrayIcon, 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 readonly ILocalizationService _localization;
|
|
|
|
private HwndSource? _source;
|
|
private ContextMenu? _menu;
|
|
private IntPtr _icon;
|
|
private bool _isInstalled;
|
|
|
|
public TrayIcon(ILocalizationService localization) => _localization = localization;
|
|
|
|
public event EventHandler? OpenRequested;
|
|
|
|
public event EventHandler? ExitRequested;
|
|
|
|
public bool Install()
|
|
{
|
|
if (_isInstalled)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
_source ??= CreateMessageWindow();
|
|
_icon = TrayIconNative.LoadApplicationIcon();
|
|
|
|
_isInstalled = TrayIconNative.Add(_source.Handle, IconId, _icon, Tooltip);
|
|
|
|
return _isInstalled;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_source is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_isInstalled)
|
|
{
|
|
TrayIconNative.Remove(_source.Handle, IconId);
|
|
_isInstalled = false;
|
|
}
|
|
|
|
TrayIconNative.ReleaseIcon(_icon);
|
|
_icon = IntPtr.Zero;
|
|
|
|
_source.RemoveHook(OnMessage);
|
|
_source.Dispose();
|
|
_source = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The menu of the icon. Built on the first ask: the application starts more
|
|
/// often than the user reaches for the menu.
|
|
/// </summary>
|
|
internal ContextMenu Menu => _menu ??= CreateMenu();
|
|
|
|
private HwndSource CreateMessageWindow()
|
|
{
|
|
// A window with no WS_VISIBLE shows nowhere, yet is a window in every other
|
|
// way. A message-only window would do as well were it not for the news of
|
|
// Explorer restarting: that one is broadcast, and broadcasts pass such
|
|
// windows by
|
|
var parameters = new HwndSourceParameters("CursorLang tray")
|
|
{
|
|
WindowStyle = unchecked((int)0x80000000), // WS_POPUP
|
|
Width = 0,
|
|
Height = 0,
|
|
};
|
|
|
|
var source = new HwndSource(parameters);
|
|
source.AddHook(OnMessage);
|
|
|
|
return source;
|
|
}
|
|
|
|
private IntPtr OnMessage(IntPtr window, int message, IntPtr wParam, IntPtr lParam, ref bool handled)
|
|
{
|
|
// Explorer has restarted and taken every icon down with it
|
|
if (message == TrayIconNative.TaskbarCreatedMessage && _isInstalled)
|
|
{
|
|
TrayIconNative.Add(window, IconId, _icon, Tooltip);
|
|
handled = true;
|
|
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
if (message != TrayIconNative.CallbackMessage)
|
|
{
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
switch (TrayIconNative.NotificationOf(lParam))
|
|
{
|
|
case TrayIconNative.SelectNotification:
|
|
case TrayIconNative.KeySelectNotification:
|
|
OpenRequested?.Invoke(this, EventArgs.Empty);
|
|
handled = true;
|
|
break;
|
|
|
|
case TrayIconNative.ContextMenuNotification:
|
|
ShowMenu();
|
|
handled = true;
|
|
break;
|
|
}
|
|
|
|
return IntPtr.Zero;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raises the menu of the icon where the pointer is.
|
|
/// </summary>
|
|
internal void ShowMenu()
|
|
{
|
|
if (_source is not null)
|
|
{
|
|
TrayIconNative.BringToForeground(_source.Handle);
|
|
}
|
|
|
|
// The menu goes where the pointer is. Windows does report the point of the
|
|
// click, but in physical pixels, while WPF places a popup in its own units —
|
|
// and the two only agree on a monitor scaled at 100%
|
|
Menu.Placement = PlacementMode.MousePoint;
|
|
Menu.IsOpen = true;
|
|
}
|
|
|
|
private ContextMenu CreateMenu()
|
|
{
|
|
var menu = new ContextMenu();
|
|
|
|
menu.Items.Add(Item("TrayMenuSettings", (_, _) => OpenRequested?.Invoke(this, EventArgs.Empty)));
|
|
menu.Items.Add(new Separator());
|
|
menu.Items.Add(Item("TrayMenuExit", (_, _) => ExitRequested?.Invoke(this, EventArgs.Empty)));
|
|
|
|
return menu;
|
|
}
|
|
|
|
// The caption is bound rather than assigned: the language is changed in the
|
|
// settings without a restart, and the menu is built once
|
|
private MenuItem Item(string key, RoutedEventHandler onClick)
|
|
{
|
|
var item = new MenuItem();
|
|
|
|
item.SetBinding(HeaderedItemsControl.HeaderProperty, new Binding($"[{key}]")
|
|
{
|
|
Source = _localization,
|
|
});
|
|
|
|
item.Click += onClick;
|
|
|
|
return item;
|
|
}
|
|
}
|