From cf0d3dd512ada3695ae2ad3b637f4522dd8786f8 Mon Sep 17 00:00:00 2001 From: Lena Date: Fri, 22 Dec 2023 13:57:01 +0100 Subject: [PATCH 1/4] create notification tray icon abstraction --- .../Platform/NotificationTrayIcon.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 osu.Framework/Platform/NotificationTrayIcon.cs diff --git a/osu.Framework/Platform/NotificationTrayIcon.cs b/osu.Framework/Platform/NotificationTrayIcon.cs new file mode 100644 index 0000000000..52e7dfd516 --- /dev/null +++ b/osu.Framework/Platform/NotificationTrayIcon.cs @@ -0,0 +1,27 @@ +using System; + +namespace osu.Framework.Platform +{ + /// + /// Represents an icon located in the OS notification tray. + /// + public abstract class NotificationTrayIcon : IDisposable + { + /// + /// The hint text shown when hovering over the icon with the cursor + /// + public string Text { get; init; } + + /// + /// The action to perform when the icon gets clicked + /// + public Action? OnClick { get; init; } + + public static NotificationTrayIcon Create(string text, Action? onClick, IWindow window) + { + throw new PlatformNotSupportedException(); + } + + public abstract void Dispose(); + } +} From 883c33ec542140d135d88d969ad2715ff3e5f76f Mon Sep 17 00:00:00 2001 From: Lena Date: Tue, 2 Jul 2024 00:20:20 +0200 Subject: [PATCH 2/4] Implement creation/removal of notification tray icons Co-authored-by: Rodrigo Pina <41023844+rodrigopina360@users.noreply.github.com> --- osu.Framework/Platform/IWindow.cs | 6 + .../Platform/NotificationTrayIcon.cs | 7 + osu.Framework/Platform/SDL2/SDL2Window.cs | 21 +++ .../Platform/SDL3/SDL3MobileWindow.cs | 11 ++ osu.Framework/Platform/SDL3/SDL3Window.cs | 21 +++ .../Platform/Windows/SDL2WindowsWindow.cs | 10 +- .../Platform/Windows/SDL3WindowsWindow.cs | 10 +- .../Windows/WindowsNotificationTrayIcon.cs | 176 ++++++++++++++++++ 8 files changed, 258 insertions(+), 4 deletions(-) create mode 100644 osu.Framework/Platform/Windows/WindowsNotificationTrayIcon.cs diff --git a/osu.Framework/Platform/IWindow.cs b/osu.Framework/Platform/IWindow.cs index d98278435b..5c45c6804a 100644 --- a/osu.Framework/Platform/IWindow.cs +++ b/osu.Framework/Platform/IWindow.cs @@ -265,5 +265,11 @@ public interface IWindow : IDisposable /// The window title. /// string Title { get; set; } + + IBindable TrayIcon { get; } + + public void CreateNotificationTrayIcon(string text, Action? onClick); + + public void RemoveNotificationTrayIcon(); } } diff --git a/osu.Framework/Platform/NotificationTrayIcon.cs b/osu.Framework/Platform/NotificationTrayIcon.cs index 52e7dfd516..294d5375af 100644 --- a/osu.Framework/Platform/NotificationTrayIcon.cs +++ b/osu.Framework/Platform/NotificationTrayIcon.cs @@ -1,4 +1,5 @@ using System; +using osu.Framework.Platform.Windows; namespace osu.Framework.Platform { @@ -19,6 +20,12 @@ public abstract class NotificationTrayIcon : IDisposable public static NotificationTrayIcon Create(string text, Action? onClick, IWindow window) { + switch (RuntimeInfo.OS) + { + case RuntimeInfo.Platform.Windows: + return new WindowsNotificationTrayIcon(text, onClick, window); + } + throw new PlatformNotSupportedException(); } diff --git a/osu.Framework/Platform/SDL2/SDL2Window.cs b/osu.Framework/Platform/SDL2/SDL2Window.cs index 2f787a506f..026fe1a5cb 100644 --- a/osu.Framework/Platform/SDL2/SDL2Window.cs +++ b/osu.Framework/Platform/SDL2/SDL2Window.cs @@ -469,6 +469,27 @@ private unsafe void setSDLIcon(Image image) }); } + public IBindable TrayIcon => trayIcon; + + private Bindable trayIcon = new Bindable(null); + + public virtual void CreateNotificationTrayIcon(string text, Action? onClick) + { + if (trayIcon.Value is not null) + { + throw new InvalidOperationException("a notification tray icon already exists!"); + } + + NotificationTrayIcon icon = NotificationTrayIcon.Create(text, onClick, this); + trayIcon.Value = icon; + } + + public virtual void RemoveNotificationTrayIcon() + { + trayIcon.Value?.Dispose(); + trayIcon.Value = null; + } + #region SDL Event Handling /// diff --git a/osu.Framework/Platform/SDL3/SDL3MobileWindow.cs b/osu.Framework/Platform/SDL3/SDL3MobileWindow.cs index ba57f6ab79..51472192d8 100644 --- a/osu.Framework/Platform/SDL3/SDL3MobileWindow.cs +++ b/osu.Framework/Platform/SDL3/SDL3MobileWindow.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using SDL; using static SDL.SDL3; @@ -20,5 +21,15 @@ protected override unsafe void UpdateWindowStateAndSize(WindowState state, Displ // Don't run base logic at all. Let's keep things simple. } + + public override void CreateNotificationTrayIcon(string text, Action? onClick) + { + throw new PlatformNotSupportedException(); + } + + public override void RemoveNotificationTrayIcon() + { + throw new PlatformNotSupportedException(); + } } } diff --git a/osu.Framework/Platform/SDL3/SDL3Window.cs b/osu.Framework/Platform/SDL3/SDL3Window.cs index 184ca2e916..613c6a082e 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window.cs @@ -440,6 +440,27 @@ private void setSDLIcon(Image image) }); } + public IBindable TrayIcon => trayIcon; + + private Bindable trayIcon = new Bindable(null); + + public virtual void CreateNotificationTrayIcon(string text, Action? onClick) + { + if (trayIcon.Value is not null) + { + throw new InvalidOperationException("a notification tray icon already exists!"); + } + + NotificationTrayIcon icon = NotificationTrayIcon.Create(text, onClick, this); + trayIcon.Value = icon; + } + + public virtual void RemoveNotificationTrayIcon() + { + trayIcon.Value?.Dispose(); + trayIcon.Value = null; + } + #region SDL Event Handling /// diff --git a/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs index 4284cbf0e1..5aac93a914 100644 --- a/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs @@ -25,8 +25,8 @@ internal class SDL2WindowsWindow : SDL2DesktopWindow, IWindowsWindow private const int large_icon_size = 256; private const int small_icon_size = 16; - private Icon? smallIcon; - private Icon? largeIcon; + internal Icon? smallIcon; + internal Icon? largeIcon; private const int wm_killfocus = 8; @@ -99,6 +99,12 @@ protected override void HandleEventFromFilter(SDL_Event e) switch (m.msg) { + case WindowsNotificationTrayIcon.TRAYICON: + if (WindowsNotificationTrayIcon.IsClick(m.lParam)) + { + TrayIcon.Value?.OnClick?.Invoke(); + } + break; case wm_killfocus: warpCursorFromFocusLoss(); break; diff --git a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs index 698c3a4192..0848199f62 100644 --- a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs @@ -29,8 +29,8 @@ internal class SDL3WindowsWindow : SDL3DesktopWindow, IWindowsWindow private const int large_icon_size = 256; private const int small_icon_size = 16; - private Icon? smallIcon; - private Icon? largeIcon; + internal Icon? smallIcon; + internal Icon? largeIcon; /// /// Whether to apply the . @@ -82,6 +82,12 @@ private SDL_bool handleEventFromHook(MSG msg) { switch (msg.message) { + case WindowsNotificationTrayIcon.TRAYICON: + if (WindowsNotificationTrayIcon.IsClick(msg.lParam)) + { + TrayIcon.Value?.OnClick?.Invoke(); + } + break; case Imm.WM_IME_STARTCOMPOSITION: case Imm.WM_IME_COMPOSITION: case Imm.WM_IME_ENDCOMPOSITION: diff --git a/osu.Framework/Platform/Windows/WindowsNotificationTrayIcon.cs b/osu.Framework/Platform/Windows/WindowsNotificationTrayIcon.cs new file mode 100644 index 0000000000..28beef93e6 --- /dev/null +++ b/osu.Framework/Platform/Windows/WindowsNotificationTrayIcon.cs @@ -0,0 +1,176 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using osu.Framework.Logging; + +namespace osu.Framework.Platform.Windows +{ + /// + /// A windows specific notification tray icon, + /// + [SupportedOSPlatform("windows")] + internal partial class WindowsNotificationTrayIcon : NotificationTrayIcon + { + internal IWindowsWindow window = null!; + + private NOTIFYICONDATAW inner; + + internal WindowsNotificationTrayIcon(string text, Action? onClick, IWindow win) + { + + if (win is not IWindowsWindow w) + { + throw new PlatformNotSupportedException(); + } + + window = w; + Text = text; + OnClick = onClick; + + NotifyIconFlags flags = NotifyIconFlags.NIF_MESSAGE | NotifyIconFlags.NIF_ICON | NotifyIconFlags.NIF_TIP | NotifyIconFlags.NIF_SHOWTIP; + IntPtr iconHandle = IntPtr.Zero; + IntPtr hwnd; + + if (window is SDL3WindowsWindow w3) + { + hwnd = w3.WindowHandle; + if (w3.largeIcon is not null) + { + iconHandle = w3.largeIcon.Handle; + } + else if (w3.smallIcon is not null) + { + iconHandle = w3.smallIcon.Handle; + } + } + else if (window is SDL2WindowsWindow w2) + { + hwnd = w2.WindowHandle; + if (w2.largeIcon is not null) + { + iconHandle = w2.largeIcon.Handle; + } + else if (w2.smallIcon is not null) + { + iconHandle = w2.smallIcon.Handle; + } + } + else + { + throw new PlatformNotSupportedException("Invalid windowing backend"); + } + + inner = new NOTIFYICONDATAW + { + cbSize = Marshal.SizeOf(inner), + uFlags = flags, + hIcon = iconHandle, + hWnd = hwnd, + szTip = text, + uCallbackMessage = TRAYICON + }; + + bool ret = Shell_NotifyIconW(NotifyIconAction.NIM_ADD, ref inner); + + inner.uTimeoutOrVersion = NOTIFYICON_VERSION_4; + + Shell_NotifyIconW(NotifyIconAction.NIM_SETVERSION, ref inner); + + if (!ret) + { + int err = Marshal.GetLastWin32Error(); + Logger.Log($"Error {err} while creating notification tray icon", LoggingTarget.Runtime, LogLevel.Error); + } + } + + public override void Dispose() + { + bool ret = Shell_NotifyIconW(NotifyIconAction.NIM_DELETE, ref inner); + + if (!ret) + { + int err = Marshal.GetLastWin32Error(); + Logger.Log($"Error {err} while removing notification tray icon", LoggingTarget.Runtime, LogLevel.Error); + } + } + + private const int NOTIFYICON_VERSION_4 = 4; + + internal const int TRAYICON = 0x0400 + 1024; + internal const int WM_LBUTTONUP = 0x0202; + internal const int WM_RBUTTONUP = 0x0205; + internal const int WM_MBUTTONUP = 0x0208; + internal const int NIN_SELECT = 0x400; + + internal static bool IsClick(long lParam) + { + switch ((short)lParam) + { + case WM_LBUTTONUP: + case WM_RBUTTONUP: + case WM_MBUTTONUP: + case NIN_SELECT: + return true; + default: + return false; + } + } + + [Flags] + internal enum NotifyIconAction : uint + { + NIM_ADD = 0x00000000, + NIM_DELETE = 0x00000002, + NIM_SETVERSION = 0x00000004, + } + + [DllImport("shell32.dll")] + internal static extern bool Shell_NotifyIconW(NotifyIconAction dwMessage, [In] ref NOTIFYICONDATAW pnid); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct NOTIFYICONDATAW + { + public int cbSize; + public IntPtr hWnd; + public int uID; + public NotifyIconFlags uFlags; + public int uCallbackMessage; + public IntPtr hIcon; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string szTip; + public int dwState; + public int dwStateMask; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string szInfo; + public int uTimeoutOrVersion; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public string szInfoTitle; + public int dwInfoFlags; + public Guid guidItem; + public IntPtr hBalloonIcon; + } + + [Flags] + public enum NotifyIconFlags : uint + { + NIF_MESSAGE = 0x00000001, + NIF_ICON = 0x00000002, + NIF_TIP = 0x00000004, + NIF_STATE = 0x00000008, + NIF_INFO = 0x00000010, + NIF_GUID = 0x00000020, + NIF_SHOWTIP = 0x00000080 + } + + internal enum ToolTipIcon + { + None = 0, + Info = 1, + Warning = 2, + Error = 3 + } + } +} From d5de496d42550a357094bd9f01247d991efb9769 Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 20 Jul 2024 16:25:26 +0200 Subject: [PATCH 3/4] fix formatting --- .../Platform/NotificationTrayIcon.cs | 2 +- .../Windows/WindowsNotificationTrayIcon.cs | 46 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/osu.Framework/Platform/NotificationTrayIcon.cs b/osu.Framework/Platform/NotificationTrayIcon.cs index 294d5375af..c4c50b4330 100644 --- a/osu.Framework/Platform/NotificationTrayIcon.cs +++ b/osu.Framework/Platform/NotificationTrayIcon.cs @@ -11,7 +11,7 @@ public abstract class NotificationTrayIcon : IDisposable /// /// The hint text shown when hovering over the icon with the cursor /// - public string Text { get; init; } + public string Text { get; init; } = string.Empty; /// /// The action to perform when the icon gets clicked diff --git a/osu.Framework/Platform/Windows/WindowsNotificationTrayIcon.cs b/osu.Framework/Platform/Windows/WindowsNotificationTrayIcon.cs index 28beef93e6..eb13684068 100644 --- a/osu.Framework/Platform/Windows/WindowsNotificationTrayIcon.cs +++ b/osu.Framework/Platform/Windows/WindowsNotificationTrayIcon.cs @@ -130,31 +130,31 @@ internal enum NotifyIconAction : uint [DllImport("shell32.dll")] internal static extern bool Shell_NotifyIconW(NotifyIconAction dwMessage, [In] ref NOTIFYICONDATAW pnid); - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct NOTIFYICONDATAW - { - public int cbSize; - public IntPtr hWnd; - public int uID; - public NotifyIconFlags uFlags; - public int uCallbackMessage; - public IntPtr hIcon; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] - public string szTip; - public int dwState; - public int dwStateMask; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] - public string szInfo; - public int uTimeoutOrVersion; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] - public string szInfoTitle; - public int dwInfoFlags; - public Guid guidItem; - public IntPtr hBalloonIcon; - } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct NOTIFYICONDATAW + { + internal int cbSize; + internal IntPtr hWnd; + internal int uID; + internal NotifyIconFlags uFlags; + internal int uCallbackMessage; + internal IntPtr hIcon; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + internal string szTip; + internal int dwState; + internal int dwStateMask; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + internal string szInfo; + internal int uTimeoutOrVersion; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + internal string szInfoTitle; + internal int dwInfoFlags; + internal Guid guidItem; + internal IntPtr hBalloonIcon; + } [Flags] - public enum NotifyIconFlags : uint + internal enum NotifyIconFlags : uint { NIF_MESSAGE = 0x00000001, NIF_ICON = 0x00000002, From b6844824696e8654407e0e6511556fd278f0dad8 Mon Sep 17 00:00:00 2001 From: Lena Date: Sun, 21 Jul 2024 13:16:49 +0200 Subject: [PATCH 4/4] only use the small icon for the notification tray --- .../Platform/Windows/WindowsNotificationTrayIcon.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/osu.Framework/Platform/Windows/WindowsNotificationTrayIcon.cs b/osu.Framework/Platform/Windows/WindowsNotificationTrayIcon.cs index eb13684068..50450d1895 100644 --- a/osu.Framework/Platform/Windows/WindowsNotificationTrayIcon.cs +++ b/osu.Framework/Platform/Windows/WindowsNotificationTrayIcon.cs @@ -37,11 +37,7 @@ internal WindowsNotificationTrayIcon(string text, Action? onClick, IWindow win) if (window is SDL3WindowsWindow w3) { hwnd = w3.WindowHandle; - if (w3.largeIcon is not null) - { - iconHandle = w3.largeIcon.Handle; - } - else if (w3.smallIcon is not null) + if (w3.smallIcon is not null) { iconHandle = w3.smallIcon.Handle; } @@ -49,11 +45,7 @@ internal WindowsNotificationTrayIcon(string text, Action? onClick, IWindow win) else if (window is SDL2WindowsWindow w2) { hwnd = w2.WindowHandle; - if (w2.largeIcon is not null) - { - iconHandle = w2.largeIcon.Handle; - } - else if (w2.smallIcon is not null) + if (w2.smallIcon is not null) { iconHandle = w2.smallIcon.Handle; }