// Copyright Epic Games, Inc. All Rights Reserved. using System.Reflection; using System.Runtime.InteropServices; using DesktopNotifications; using DesktopNotifications.Windows; using EpicGames.Core; namespace UnrealToolbox { /// /// A tool notification /// class ToolboxNotification { /// /// The title to display /// public string Title { get; } /// /// The body of the notification /// public string Body { get; } /// /// Whether to force the notification regardless of delta time /// public bool Force { get; } /// /// Constructor /// /// /// /// public ToolboxNotification(string title, string body, bool force = false) { Title = title; Body = body; Force = force; } } /// /// Toolbox notification manager implementation /// class ToolboxNotificationManager : IAsyncDisposable { private INotificationManager? _platformManager; private static readonly object s_lock = new object(); private static List s_notifications = new List(); readonly BackgroundTask _backgroundTask; // spam prevention DateTime? _lastNotificationTime; string? _lastTitle; string? _lastBody; [DllImport("shell32.dll", SetLastError = true)] private static extern void SetCurrentProcessExplicitAppUserModelID([MarshalAs(UnmanagedType.LPWStr)] string appId); public ToolboxNotificationManager() { _backgroundTask = new BackgroundTask(PostNotificationsAsync); } async Task PostNotificationsAsync(CancellationToken cancellationToken) { for (; ; ) { try { List? notifications = null; lock (s_lock) { if (s_notifications.Count > 0) { notifications = s_notifications; s_notifications = new List(); } } if (notifications != null && notifications.Count > 0) { notifications.Reverse(); ShowNotification(notifications[0].Title, notifications[0].Body, notifications[0].Force); } } catch (OperationCanceledException) { throw; } catch (Exception) { } await Task.Delay(TimeSpan.FromSeconds(1.0), cancellationToken); } } public void Start() { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { // WindowsApplicationContext.FromCurrentProcess() has side effects of creating start menu items, and changing the app user model id to the executing assembly, which can be dotnet.exe // WindowsApplicationContext context = WindowsApplicationContext.FromCurrentProcess(); WindowsApplicationContext? context = Activator.CreateInstance(type: typeof(WindowsApplicationContext), bindingAttr: BindingFlags.Instance | BindingFlags.NonPublic, binder: null, args: new object[] { "Unreal Toolbox", "Unreal Toolbox" }, culture: null) as WindowsApplicationContext; SetCurrentProcessExplicitAppUserModelID("Unreal Toolbox"); _platformManager = new WindowsNotificationManager(context); } else { throw new NotImplementedException(); } // initialize and ensure with result _platformManager.Initialize().GetAwaiter().GetResult(); _backgroundTask.Start(); } public async ValueTask DisposeAsync() { _platformManager?.Dispose(); await _backgroundTask.DisposeAsync(); } /// /// Threead safe notification posting /// /// /// /// public static void PostNotification(string title, string body, bool force = false) { lock (s_lock) { s_notifications.Add(new ToolboxNotification(title, body, force)); } } /// /// Show a notification, /// /// /// /// private void ShowNotification(string title, string body, bool force = false) { if (_platformManager == null) { return; } // spawn if (!force && _lastNotificationTime != null && !String.IsNullOrEmpty(_lastBody) && !String.IsNullOrEmpty(_lastTitle)) { TimeSpan deltaTime = DateTime.Now - _lastNotificationTime.Value; // don't show a new notification if already displayed one in last 2 minutes if (deltaTime.TotalSeconds < 120) { return; } // if the title and body are the same, wait 10 minutes if ((deltaTime.TotalSeconds < 600) && title == _lastTitle && body == _lastBody) { return; } } _lastTitle = title; _lastBody = body; _lastNotificationTime = DateTime.Now; Notification notification = new Notification { Title = title, Body = body }; _platformManager.ShowNotification(notification, DateTimeOffset.Now + TimeSpan.FromSeconds(30)); } } }