// 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(remove: 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;
}
}