// Author: Gockner, Simon // Created: 2021-04-06 // Copyright(c) 2021 SimonG. All Rights Reserved. using System; using System.Collections.Generic; using System.Drawing; using Avalonia; using Avalonia.Controls; using Avalonia.Platform; using Lib.NotifyIcon.Windows.Native; using Lib.NotifyIcon.Windows.Native.Types; using Point = Lib.NotifyIcon.Windows.Native.Types.Point; namespace Lib.NotifyIcon.Windows { public class NotifyIcon : INotifyIcon { private static int _nextUId; private readonly NotifyIconHelperWindow _helperWindow; private readonly int _uId; private bool _doubleClick; private bool _iconAdded; private Icon _icon; private string _iconPath = ""; private string _toolTipText = ""; private bool _visible; /// /// Creates a new instance and sets up some required resources /// public NotifyIcon() { _uId = ++_nextUId; _helperWindow = new NotifyIconHelperWindow(this); } ~NotifyIcon() => UpdateIcon(true); /// /// Gets or sets the path for the notify icon /// public string IconPath { get => _iconPath; set { _iconPath = value; IAssetLoader assetLoader = AvaloniaLocator.Current.GetService(); _icon = new Icon(assetLoader.Open(new Uri(_iconPath))); UpdateIcon(); } } /// /// Gets or sets the tooltip text for the notify icon /// public string ToolTipText { get => _toolTipText; set { _toolTipText = value; UpdateIcon(); } } /// /// Gets or sets the context menu for the notify icon /// public ContextMenu ContextMenu { get; set; } /// /// Gets or sets if the notify icon is visible in the taskbar notification area or not /// public bool Visible { get => _visible; set { _visible = value; UpdateIcon(); } } /// /// Removes the notify icon from the taskbar notification area /// public void Remove() => UpdateIcon(true); /// /// Handles the NotifyIcon-specific window messages sent by the notification icon /// public void WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { //We only care about tray icon messages if (msg != (uint) CustomWindowsMessage.WmTrayMouse) return; //Determine the type of message and call the matching event handlers switch (lParam.ToInt32()) { case (int) WindowsMessage.WmLButtonUp: { if (!_doubleClick) Click?.Invoke(this, new EventArgs()); _doubleClick = false; break; } case (int) WindowsMessage.WmLButtonDblClk: { DoubleClick?.Invoke(this, new EventArgs()); _doubleClick = true; break; } case (int) WindowsMessage.WmRButtonUp: { EventArgs e = new EventArgs(); RightClick?.Invoke(this, e); ShowContextMenu(); break; } } } /// /// Shows, hides or removes the notify icon based on the set properties and parameters /// /// If set to true, the notify icon will be removed private void UpdateIcon(bool remove = false) { NotifyIconData iconData = new() { hWnd = _helperWindow.Handle, uID = _uId, uFlags = NIF.Tip | NIF.Message, uCallbackMessage = (int) CustomWindowsMessage.WmTrayMouse, hIcon = IntPtr.Zero, szTip = ToolTipText }; if (!remove && _icon != null && Visible) { iconData.uFlags |= NIF.Icon; iconData.hIcon = _icon.Handle; if (!_iconAdded) { WindowApi.Shell_NotifyIcon(NIM.Add, iconData); _iconAdded = true; } else WindowApi.Shell_NotifyIcon(NIM.Modify, iconData); } else { WindowApi.Shell_NotifyIcon(NIM.Delete, iconData); _iconAdded = false; } } /// /// If available, displays the notification icon's context menu /// private void ShowContextMenu() { if (ContextMenu == null) return; // Since we can't use the Avalonia ContextMenu directly due to shortcomings // regrading its positioning, we'll create a native context menu instead. // This dictionary will map the menu item IDs which we'll need for the native // menu to the MenuItems of the provided Avalonia ContextMenu. Dictionary contextItemLookup = new(); // Create a native (Win32) popup menu as the notify icon's context menu. IntPtr popupMenu = WindowApi.CreatePopupMenu(); uint i = 1; foreach (var item in ContextMenu.Items) { if (item is MenuItem menuItem) { // Add items to the native context menu by simply reusing // the information provided within the Avalonia ContextMenu. WindowApi.AppendMenu(popupMenu, MenuFlags.MfString, i, (string) menuItem.Header); // Add the mapping so that we can find the selected item later contextItemLookup.Add(i, menuItem); } else if (item is Separator) WindowApi.AppendMenu(popupMenu, MenuFlags.MfSeparator, i, null); i++; } // To display a context menu for a notification icon, the current window // must be the foreground window before the application calls TrackPopupMenu // or TrackPopupMenuEx. Otherwise, the menu will not disappear when the user // clicks outside of the menu or the window that created the menu (if it is // visible). If the current window is a child window, you must set the // (top-level) parent window as the foreground window. WindowApi.SetForegroundWindow(_helperWindow.Handle); // Get the mouse cursor position WindowApi.GetCursorPos(out Point pt); // Now display the context menu and block until we get a result uint commandId = WindowApi.TrackPopupMenuEx(popupMenu, UFlags.TpmBottomAlign | UFlags.TpmRightAlign | UFlags.TpmNoNotify | UFlags.TpmReturnCmd, pt.X, pt.Y, _helperWindow.Handle, IntPtr.Zero); // If we have a result, execute the corresponding command if (commandId != 0) contextItemLookup[commandId].Command?.Execute(null); } public event EventHandler Click; public event EventHandler DoubleClick; public event EventHandler RightClick; } }