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 24bf9b2..b486d5f 100644 Binary files a/NotificationIcon.NET/runtimes/win-x64/native/notification_icon.dll and b/NotificationIcon.NET/runtimes/win-x64/native/notification_icon.dll differ diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index 34f2878..eab83b7 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -1,7 +1,7 @@ -cmake_minimum_required(VERSION 3.0.0) - +cmake_minimum_required(VERSION 3.5.0) + project(notification_icon VERSION 2.0.0) - + add_library(notification_icon SHARED "src/main.c") if(UNIX AND NOT APPLE) find_package(PkgConfig REQUIRED) diff --git a/native/src/tray.h b/native/src/tray.h index 184f87b..c3b0ce8 100644 --- a/native/src/tray.h +++ b/native/src/tray.h @@ -1,6 +1,7 @@ /* MIT License +Copyright (c) 2026 Roman Anderson Copyright (c) 2023 Ori Almagor Copyright (c) 2017 Serge Zaitsev @@ -29,6 +30,8 @@ struct tray_menu; struct tray { char* icon; + char* tooltip; + void (*double_click_cb)(struct tray*); struct tray_menu* menu; }; diff --git a/native/src/tray_windows.h b/native/src/tray_windows.h index baebbf0..3485ff1 100644 --- a/native/src/tray_windows.h +++ b/native/src/tray_windows.h @@ -27,6 +27,7 @@ SOFTWARE. #define UNICODE #include +#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