From 25ca17bbe7733906cae0f5b4dd7ad2105d636283 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 31 Mar 2026 15:29:34 +0400 Subject: [PATCH] feat : add new feature doubleClick LMB, context menu RMB update : Demo project update, add pseudo animation --- NotificationIcon.NET/LinuxNotifyIcon.cs | 6 ++ .../NotificationIcon.NET.csproj | 1 + NotificationIcon.NET/NotifyIcon.cs | 79 ++++++++++++++++-- .../TrayIconClickEventArgs.cs | 20 +++++ NotificationIcon.NET/WindowsNotifyIcon.cs | 7 +- .../win-x64/native/notification_icon.dll | Bin 12288 -> 12288 bytes native/CMakeLists.txt | 6 +- native/src/tray.h | 3 + native/src/tray_windows.h | 35 ++++++-- 9 files changed, 141 insertions(+), 16 deletions(-) create mode 100644 NotificationIcon.NET/TrayIconClickEventArgs.cs diff --git a/NotificationIcon.NET/LinuxNotifyIcon.cs b/NotificationIcon.NET/LinuxNotifyIcon.cs index 9b24f92..c59bbe9 100644 --- a/NotificationIcon.NET/LinuxNotifyIcon.cs +++ b/NotificationIcon.NET/LinuxNotifyIcon.cs @@ -35,6 +35,10 @@ private struct Tray { [MarshalAs(UnmanagedType.LPUTF8Str)] public string iconPath; + // tooltip and double_click_cb are in the shared native struct layout but unused on Linux + [MarshalAs(UnmanagedType.LPUTF8Str)] + public string? tooltip; + public IntPtr doubleClickCb; public IntPtr menus; } @@ -106,6 +110,8 @@ protected override IHeapAlloc AllocateTray(nint menuItemsPtr) Tray tray = new() { iconPath = IconPath, + tooltip = null, // Not supported on Linux/AppIndicator + doubleClickCb = IntPtr.Zero, menus = menuItemsPtr }; return HeapAlloc.Copy(tray); diff --git a/NotificationIcon.NET/NotificationIcon.NET.csproj b/NotificationIcon.NET/NotificationIcon.NET.csproj index 57b8347..e329352 100644 --- a/NotificationIcon.NET/NotificationIcon.NET.csproj +++ b/NotificationIcon.NET/NotificationIcon.NET.csproj @@ -21,6 +21,7 @@ win-x64;linux-x64; true $(MSBuildProjectDirectory)/bin/$(Configuration)/$(TargetFramework)/NotificationIcon.NET.xml + 12 diff --git a/NotificationIcon.NET/NotifyIcon.cs b/NotificationIcon.NET/NotifyIcon.cs index dc4068c..ebae29a 100644 --- a/NotificationIcon.NET/NotifyIcon.cs +++ b/NotificationIcon.NET/NotifyIcon.cs @@ -26,6 +26,9 @@ public abstract class NotifyIcon : IDisposable [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void MenuItemCallback(IntPtr menu); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void TrayCallback(IntPtr tray); + /// /// A path to an icon on the file system. For Windows, this should be an ICO file. For Unix, this should be a PNG. /// @@ -43,6 +46,35 @@ public string IconPath } private string _iconPath; + /// + /// The tooltip text shown when hovering over the tray icon. Null means no tooltip. + /// On Linux this property is silently ignored (AppIndicator does not support tooltips). + /// + public string? Tooltip + { + get => _tooltip; + set + { + if (!string.Equals(_tooltip, value)) + { + _tooltip = value; + AllocateNewTray(true); + } + } + } + private string? _tooltip; + + /// + /// Returns true if there are any subscribers to the event. + /// + protected bool HasDoubleClickSubscribers => DoubleClick != null; + + /// + /// Raised when the user double-clicks the tray icon. + /// On Linux this event is never raised (AppIndicator does not support click events). + /// + public event EventHandler? DoubleClick; + /// /// The currently displayed menu items. /// @@ -59,6 +91,7 @@ public IReadOnlyList MenuItems private IReadOnlyList _menuItems; private readonly List _callbacks; + private TrayCallback? _doubleClickCallback; private IHeapAlloc trayHandle; private IHeapAlloc menuItemsHandle; private Exception? trayLoopException; @@ -69,15 +102,28 @@ public IReadOnlyList MenuItems /// /// A path to an icon on the file system. For Windows, this should be an ICO file. For Unix, this should be a PNG. /// The menu items to display to the user when clicking the icon. + /// Optional tooltip shown when hovering over the icon. Ignored on Linux. + /// Optional action invoked on double-click. Ignored on Linux. /// /// - public static NotifyIcon Create(string iconPath, IReadOnlyList menuItems) + public static NotifyIcon Create( + string iconPath, + IReadOnlyList menuItems, + string? tooltip = null, + Action? onDoubleClick = null) { + NotifyIcon icon; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return new WindowsNotifyIcon(iconPath, menuItems); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - return new LinuxNotifyIcon(iconPath, menuItems); - throw new PlatformNotSupportedException(); + icon = new WindowsNotifyIcon(iconPath, menuItems); + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + icon = new LinuxNotifyIcon(iconPath, menuItems); + else + throw new PlatformNotSupportedException(); + + icon.Tooltip = tooltip; + if (onDoubleClick != null) + icon.DoubleClick += (s, e) => onDoubleClick(icon); + return icon; } /// @@ -268,6 +314,28 @@ protected nint CreateNativeClickCallback(MenuItem menuItem) return Marshal.GetFunctionPointerForDelegate(callback); } + /// + /// Creates (or returns cached) a native callback for double-click events. + /// + protected nint GetOrCreateNativeDoubleClickCallback() + { + if (_doubleClickCallback == null) + { + _doubleClickCallback = trayPtr => + { + try + { + DoubleClick?.Invoke(this, new TrayIconClickEventArgs(this)); + } + catch (Exception ex) + { + trayLoopException = ex; + } + }; + } + return Marshal.GetFunctionPointerForDelegate(_doubleClickCallback); + } + /// /// Shows this icon in the notification area and reacts to user events. /// Keeps blocking the thread as long as this icon is shown. @@ -297,6 +365,7 @@ public virtual void Dispose() trayHandle.Dispose(); menuItemsHandle.Dispose(); _callbacks.Clear(); + _doubleClickCallback = null; disposed = true; } } diff --git a/NotificationIcon.NET/TrayIconClickEventArgs.cs b/NotificationIcon.NET/TrayIconClickEventArgs.cs new file mode 100644 index 0000000..a2a2f77 --- /dev/null +++ b/NotificationIcon.NET/TrayIconClickEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace NotificationIcon.NET +{ + /// + /// Event arguments for tray icon click events (e.g. double-click). + /// + public class TrayIconClickEventArgs : EventArgs + { + /// + /// The that was clicked. + /// + public NotifyIcon Icon { get; } + + public TrayIconClickEventArgs(NotifyIcon icon) + { + Icon = icon; + } + } +} \ No newline at end of file diff --git a/NotificationIcon.NET/WindowsNotifyIcon.cs b/NotificationIcon.NET/WindowsNotifyIcon.cs index 0b4e0bf..c0eb0a4 100644 --- a/NotificationIcon.NET/WindowsNotifyIcon.cs +++ b/NotificationIcon.NET/WindowsNotifyIcon.cs @@ -35,6 +35,9 @@ private struct Tray { [MarshalAs(UnmanagedType.LPWStr)] public string iconPath; + [MarshalAs(UnmanagedType.LPWStr)] + public string? tooltip; + public IntPtr doubleClickCb; public IntPtr menus; } @@ -106,8 +109,10 @@ protected override IHeapAlloc AllocateTray(nint menuItemsPtr) Tray tray = new() { iconPath = IconPath, + tooltip = Tooltip, + doubleClickCb = GetOrCreateNativeDoubleClickCallback(), menus = menuItemsPtr }; return HeapAlloc.Copy(tray); } -} +} \ No newline at end of file diff --git a/NotificationIcon.NET/runtimes/win-x64/native/notification_icon.dll b/NotificationIcon.NET/runtimes/win-x64/native/notification_icon.dll index 24bf9b28127fadc4472fc26507cba377bfb9eb70..b486d5fd18b457db4423dc546b47843a637e788f 100644 GIT binary patch literal 12288 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 literal 12288 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 J_isM}{{?VsJk +#include #include #define EXPORT __declspec(dllexport) @@ -40,6 +41,8 @@ static HWND hwnd; static HMENU hmenu = NULL; static LONG isTrayLoopRunning = 0; +static struct tray* _current_tray = NULL; + static LRESULT CALLBACK _tray_wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { switch (msg) { case WM_CLOSE: @@ -49,11 +52,18 @@ static LRESULT CALLBACK _tray_wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARA PostQuitMessage(0); return 0; case WM_TRAY_CALLBACK_MESSAGE: - if (lparam == WM_LBUTTONUP || lparam == WM_RBUTTONUP) { - POINT p; - if (!GetCursorPos(&p)) { - break; + { + UINT mouseEvent = LOWORD(lparam); + if (mouseEvent == WM_LBUTTONDBLCLK) { + if (_current_tray != NULL && _current_tray->double_click_cb != NULL) { + _current_tray->double_click_cb(_current_tray); } + return 0; + } + if (mouseEvent == WM_RBUTTONUP) { + POINT p; + p.x = GET_X_LPARAM(wparam); + p.y = GET_Y_LPARAM(wparam); SetForegroundWindow(hwnd); if (hmenu == NULL) { break; @@ -69,6 +79,7 @@ static LRESULT CALLBACK _tray_wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARA return 0; } break; + } case WM_COMMAND: { UINT menuItemId = (UINT)wparam; @@ -127,6 +138,7 @@ static HMENU _tray_menu(struct tray_menu* m, UINT* id) { } EXPORT void tray_update(struct tray* tray) { + _current_tray = tray; HMENU prevmenu = hmenu; UINT id = ID_TRAY_FIRST; hmenu = _tray_menu(tray->menu, &id); @@ -137,6 +149,12 @@ EXPORT void tray_update(struct tray* tray) { DestroyIcon(nid.hIcon); } nid.hIcon = icon; + nid.uFlags |= NIF_TIP; + if (tray->tooltip != NULL) { + wcscpy_s(nid.szTip, sizeof(nid.szTip) / sizeof(WCHAR), (LPCWSTR)tray->tooltip); + } else { + nid.szTip[0] = L'\0'; + } Shell_NotifyIcon(NIM_MODIFY, &nid); if (prevmenu != NULL) { @@ -166,10 +184,14 @@ EXPORT INT32 tray_init(struct tray* tray) { nid.cbSize = sizeof(NOTIFYICONDATA); nid.hWnd = hwnd; nid.uID = 0; - nid.uFlags = NIF_ICON | NIF_MESSAGE; + nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; nid.uCallbackMessage = WM_TRAY_CALLBACK_MESSAGE; Shell_NotifyIcon(NIM_ADD, &nid); + // Enable version 4 to receive WM_LBUTTONDBLCLK and extended cursor position in wparam + nid.uVersion = NOTIFYICON_VERSION_4; + Shell_NotifyIcon(NIM_SETVERSION, &nid); + tray_update(tray); return 0; } @@ -217,5 +239,4 @@ EXPORT void tray_exit() { while (tray_loop(1) == 0) { } } -} - \ No newline at end of file +} \ No newline at end of file