From eabaa776cc450a0ae551d9d68f75643f48eadf1a Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 11 Jul 2023 16:29:41 +0200 Subject: [PATCH 01/13] feat: Customize Notifications --- .../ViewModels/NotificationViewModel.cs | 2 +- .../IManagedNotificationManager.cs | 13 +++- .../Notifications/NotificationCard.cs | 65 ++++++++++++++++++- .../WindowNotificationManager.cs | 24 +++---- 4 files changed, 88 insertions(+), 16 deletions(-) diff --git a/samples/ControlCatalog/ViewModels/NotificationViewModel.cs b/samples/ControlCatalog/ViewModels/NotificationViewModel.cs index bcbcb345ef..84a3c74dbf 100644 --- a/samples/ControlCatalog/ViewModels/NotificationViewModel.cs +++ b/samples/ControlCatalog/ViewModels/NotificationViewModel.cs @@ -11,7 +11,7 @@ namespace ControlCatalog.ViewModels { ShowCustomManagedNotificationCommand = MiniCommand.Create(() => { - NotificationManager?.Show(new NotificationViewModel() { Title = "Hey There!", Message = "Did you know that Avalonia now supports Custom In-Window Notifications?" , NotificationManager = NotificationManager}); + NotificationManager?.Show(new NotificationViewModel() { Title = "Hey There!", Message = "Did you know that Avalonia now supports Custom In-Window Notifications?" , NotificationManager = NotificationManager}, NotificationType.Warning); }); ShowManagedNotificationCommand = MiniCommand.Create(() => diff --git a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs index b2e6e9e80b..8d1cb644be 100644 --- a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs @@ -1,4 +1,5 @@ -using Avalonia.Metadata; +using System; +using Avalonia.Metadata; namespace Avalonia.Controls.Notifications { @@ -18,6 +19,14 @@ namespace Avalonia.Controls.Notifications /// Shows a notification. /// /// The content to be displayed. - void Show(object content); + /// The of the notification. + /// the expiration time of the notification after which it will automatically close. If the value is then the notification will remain open until the user closes it. + /// an Action to be run when the notification is clicked. + /// an Action to be run when the notification is closed. + void Show(object content, + NotificationType type = NotificationType.Information, + TimeSpan? expiration = null, + Action? onClick = null, + Action? onClose = null); } } diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index 7d5b6cc0ca..da9e15a7f3 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Avalonia.Reactive; using Avalonia.Controls.Metadata; @@ -61,6 +61,7 @@ namespace Avalonia.Controls.Notifications } } }); + UpdateNotificationType(); } /// @@ -93,6 +94,21 @@ namespace Avalonia.Controls.Notifications public static readonly StyledProperty IsClosedProperty = AvaloniaProperty.Register(nameof(IsClosed)); + /// + /// Gets or sets the type of the notification + /// + public NotificationType NotificationType + { + get { return GetValue(NotificationTypeProperty); } + set { SetValue(NotificationTypeProperty, value); } + } + + /// + /// Defines the property + /// + public static readonly StyledProperty NotificationTypeProperty = + AvaloniaProperty.Register(nameof(NotificationType)); + /// /// Defines the event. /// @@ -163,5 +179,52 @@ namespace Avalonia.Controls.Notifications IsClosing = true; } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.Property == ContentProperty && e.NewValue is INotification notification) + { + SetValue(NotificationTypeProperty, notification.Type); + } + + if (e.Property == NotificationTypeProperty) + { + UpdateNotificationType(); + } + + if (e.Property == IsClosedProperty) + { + if (!IsClosing && !IsClosed) + { + return; + } + + RaiseEvent(new RoutedEventArgs(NotificationClosedEvent)); + } + } + + private void UpdateNotificationType() + { + switch (NotificationType) + { + case NotificationType.Error: + PseudoClasses.Add(":error"); + break; + + case NotificationType.Information: + PseudoClasses.Add(":information"); + break; + + case NotificationType.Success: + PseudoClasses.Add(":success"); + break; + + case NotificationType.Warning: + PseudoClasses.Add(":warning"); + break; + } + } } } diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index b03099f750..d764f2bd78 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -80,32 +80,32 @@ namespace Avalonia.Controls.Notifications /// public void Show(INotification content) { - Show(content as object); + Show(content, content.Type, content.Expiration, content.OnClick, content.OnClose); } /// - public async void Show(object content) + public async void Show(object content, + NotificationType type = NotificationType.Information, + TimeSpan? expiration = null, + Action? onClick = null, + Action? onClose = null) { - var notification = content as INotification; - var notificationControl = new NotificationCard { - Content = content + Content = content, + NotificationType = type }; notificationControl.NotificationClosed += (sender, args) => { - notification?.OnClose?.Invoke(); + onClose?.Invoke(); _items?.Remove(sender); }; notificationControl.PointerPressed += (sender, args) => { - if (notification != null && notification.OnClick != null) - { - notification.OnClick.Invoke(); - } + onClick?.Invoke(); (sender as NotificationCard)?.Close(); }; @@ -117,12 +117,12 @@ namespace Avalonia.Controls.Notifications _items.OfType().First(i => !i.IsClosing).Close(); } - if (notification != null && notification.Expiration == TimeSpan.Zero) + if (expiration == TimeSpan.Zero) { return; } - await Task.Delay(notification?.Expiration ?? TimeSpan.FromSeconds(5)); + await Task.Delay(expiration ?? TimeSpan.FromSeconds(5)); notificationControl.Close(); } From 7514d7085c8b60583d59de86d5d87048197e3eec Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 13 Jul 2023 16:02:43 +0200 Subject: [PATCH 02/13] Added an option to specify classes --- .../Notifications/IManagedNotificationManager.cs | 4 +++- .../Notifications/WindowNotificationManager.cs | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs index 8d1cb644be..5eba36dbe6 100644 --- a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs @@ -23,10 +23,12 @@ namespace Avalonia.Controls.Notifications /// the expiration time of the notification after which it will automatically close. If the value is then the notification will remain open until the user closes it. /// an Action to be run when the notification is clicked. /// an Action to be run when the notification is closed. + /// Style-classes to ba added to the notification card void Show(object content, NotificationType type = NotificationType.Information, TimeSpan? expiration = null, Action? onClick = null, - Action? onClose = null); + Action? onClose = null, + string[]? classes = null); } } diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index d764f2bd78..0b7a19707b 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -88,7 +88,8 @@ namespace Avalonia.Controls.Notifications NotificationType type = NotificationType.Information, TimeSpan? expiration = null, Action? onClick = null, - Action? onClose = null) + Action? onClose = null, + string[]? classes = null) { var notificationControl = new NotificationCard { @@ -96,6 +97,15 @@ namespace Avalonia.Controls.Notifications NotificationType = type }; + // Add style classes if any + if (classes != null) + { + foreach (var @class in classes) + { + notificationControl.Classes.Add(@class); + } + } + notificationControl.NotificationClosed += (sender, args) => { onClose?.Invoke(); From 540bade45a0d4c31fbb0dcb6483e6b68c9355e5e Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 21 Jul 2023 15:00:53 +0200 Subject: [PATCH 03/13] Improve WinodwNotificationManager - Allow usage from XAML - Allow any Visual as Host, so it can be used inside UserControl for example - Allow adding style classes for NotificationCards --- .../WindowNotificationManager.cs | 78 +++++++++++++++---- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 0b7a19707b..80931d1865 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -1,11 +1,10 @@ using System; using System.Collections; using System.Linq; -using Avalonia.Reactive; using System.Threading.Tasks; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Rendering; +using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls.Notifications @@ -18,6 +17,7 @@ namespace Avalonia.Controls.Notifications public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager { private IList? _items; + private AdornerLayer? adornerLayer; /// /// Defines the property. @@ -50,17 +50,40 @@ namespace Avalonia.Controls.Notifications set { SetValue(MaxItemsProperty, value); } } + /// + /// Defines the property + /// + public static readonly DirectProperty HostProperty = + AvaloniaProperty.RegisterDirect( + nameof(Host), + o => o.Host, + (o, v) => o.Host = v); + + private Visual? _Host; + + /// + /// The Host that this NotificationManger should register to. If the Host is null, the Parent will be used. + /// + public Visual? Host + { + get { return _Host; } + set { SetAndRaise(HostProperty, ref _Host, value); } + } + /// /// Initializes a new instance of the class. /// - /// The window that will host the control. - public WindowNotificationManager(TopLevel? host) + /// The visual that will host the control. + public WindowNotificationManager(Visual? host) : this() { - if (host != null) - { - Install(host); - } + Host = host; + } + /// + /// Initializes a new instance of the class. + /// + public WindowNotificationManager() + { UpdatePseudoClasses(Position); } @@ -73,6 +96,8 @@ namespace Avalonia.Controls.Notifications /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { + base.OnApplyTemplate(e); + var itemsControl = e.NameScope.Find("PART_Items"); _items = itemsControl?.Children; } @@ -120,12 +145,15 @@ namespace Avalonia.Controls.Notifications (sender as NotificationCard)?.Close(); }; - _items?.Add(notificationControl); - - if (_items?.OfType().Count(i => !i.IsClosing) > MaxItems) + Dispatcher.UIThread.Post(() => { - _items.OfType().First(i => !i.IsClosing).Close(); - } + _items?.Add(notificationControl); + + if (_items?.OfType().Count(i => !i.IsClosing) > MaxItems) + { + _items.OfType().First(i => !i.IsClosing).Close(); + } + }); if (expiration == TimeSpan.Zero) { @@ -145,16 +173,32 @@ namespace Avalonia.Controls.Notifications { UpdatePseudoClasses(change.GetNewValue()); } + + if (change.Property == HostProperty) + { + Install(); + } } /// /// Installs the within the - /// of the host . /// - /// The that will be the host. - private void Install(TemplatedControl host) + private void Install() { - var adornerLayer = host.FindDescendantOfType()?.AdornerLayer; + // unregister from AdornerLayer if this control was already installed + if (adornerLayer is not null && !adornerLayer.Children.Contains(this)) + { + adornerLayer.Children.Remove(this); + } + + // Try to get the host. If host was null, use the TopLevel instead. + var host = Host ?? Parent as Visual; + + if (host is null) throw new InvalidOperationException("NotificationControl cannot be installed. Host was not found."); + + adornerLayer = host is TopLevel + ? host.FindDescendantOfType()?.AdornerLayer + : AdornerLayer.GetAdornerLayer(host); if (adornerLayer is not null) { From eadf63ce450a5daa9f61e8fd72f995616210ee6a Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 24 Jul 2023 12:15:49 +0200 Subject: [PATCH 04/13] Revert breaking change --- .../IManagedNotificationManager.cs | 8 ++++++- .../WindowNotificationManager.cs | 21 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs index 5eba36dbe6..94d927f8ca 100644 --- a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs @@ -15,6 +15,12 @@ namespace Avalonia.Controls.Notifications [NotClientImplementable] public interface IManagedNotificationManager : INotificationManager { + /// + /// Shows a notification. + /// + /// The content to be displayed. + void Show(object content); + /// /// Shows a notification. /// @@ -25,7 +31,7 @@ namespace Avalonia.Controls.Notifications /// an Action to be run when the notification is closed. /// Style-classes to ba added to the notification card void Show(object content, - NotificationType type = NotificationType.Information, + NotificationType type, TimeSpan? expiration = null, Action? onClick = null, Action? onClose = null, diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 80931d1865..26ca8abb42 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -73,8 +73,8 @@ namespace Avalonia.Controls.Notifications /// /// Initializes a new instance of the class. /// - /// The visual that will host the control. - public WindowNotificationManager(Visual? host) : this() + /// The TopLevel that will host the control. + public WindowNotificationManager(TopLevel? host) : this() { Host = host; } @@ -108,9 +108,22 @@ namespace Avalonia.Controls.Notifications Show(content, content.Type, content.Expiration, content.OnClick, content.OnClose); } + /// + public async void Show(object content) + { + if (content is INotification notification) + { + Show(notification, notification.Type, notification.Expiration, notification.OnClick, notification.OnClose); + } + else + { + Show(content, NotificationType.Information); + } + } + /// public async void Show(object content, - NotificationType type = NotificationType.Information, + NotificationType type, TimeSpan? expiration = null, Action? onClick = null, Action? onClose = null, @@ -202,7 +215,7 @@ namespace Avalonia.Controls.Notifications if (adornerLayer is not null) { - adornerLayer.Children.Add(this); + // adornerLayer.Children.Add(this); AdornerLayer.SetAdornedElement(this, adornerLayer); } } From 7477aa7330fad59caa68e475e09ace45b0698946 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 24 Jul 2023 13:03:08 +0200 Subject: [PATCH 05/13] Revert more breaking changes --- .../Notifications/IManagedNotificationManager.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs index 94d927f8ca..bd57f3a86f 100644 --- a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs @@ -20,21 +20,5 @@ namespace Avalonia.Controls.Notifications /// /// The content to be displayed. void Show(object content); - - /// - /// Shows a notification. - /// - /// The content to be displayed. - /// The of the notification. - /// the expiration time of the notification after which it will automatically close. If the value is then the notification will remain open until the user closes it. - /// an Action to be run when the notification is clicked. - /// an Action to be run when the notification is closed. - /// Style-classes to ba added to the notification card - void Show(object content, - NotificationType type, - TimeSpan? expiration = null, - Action? onClick = null, - Action? onClose = null, - string[]? classes = null); } } From e83ebc2bb33315b7c033d02fcfd0fae0d455bcf6 Mon Sep 17 00:00:00 2001 From: Tim <47110241+timunie@users.noreply.github.com> Date: Wed, 30 Aug 2023 15:34:05 +0200 Subject: [PATCH 06/13] Handle merge conflicts comming from current master --- .../Notifications/NotificationCard.cs | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index da9e15a7f3..d233525056 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -25,42 +25,6 @@ namespace Avalonia.Controls.Notifications /// public NotificationCard() { - this.GetObservable(IsClosedProperty) - .Subscribe(x => - { - if (!IsClosing && !IsClosed) - { - return; - } - - RaiseEvent(new RoutedEventArgs(NotificationClosedEvent)); - }); - - this.GetObservable(ContentProperty) - .Subscribe(x => - { - if (x is INotification notification) - { - switch (notification.Type) - { - case NotificationType.Error: - PseudoClasses.Add(":error"); - break; - - case NotificationType.Information: - PseudoClasses.Add(":information"); - break; - - case NotificationType.Success: - PseudoClasses.Add(":success"); - break; - - case NotificationType.Warning: - PseudoClasses.Add(":warning"); - break; - } - } - }); UpdateNotificationType(); } From 3f0769a1f44c49aa8e933dfd0ffc2cecfcdda9cb Mon Sep 17 00:00:00 2001 From: Tim <47110241+timunie@users.noreply.github.com> Date: Wed, 30 Aug 2023 15:46:37 +0200 Subject: [PATCH 07/13] Uncomment code that was commented by accident --- .../Notifications/WindowNotificationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 26ca8abb42..cb6a844d32 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -215,7 +215,7 @@ namespace Avalonia.Controls.Notifications if (adornerLayer is not null) { - // adornerLayer.Children.Add(this); + adornerLayer.Children.Add(this); AdornerLayer.SetAdornedElement(this, adornerLayer); } } From bfeeb10adf8fa52c4f358ecf6c9c7d8f66dbcf37 Mon Sep 17 00:00:00 2001 From: Tim <47110241+timunie@users.noreply.github.com> Date: Wed, 30 Aug 2023 15:46:57 +0200 Subject: [PATCH 08/13] improved help text --- .../Notifications/WindowNotificationManager.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index cb6a844d32..889e17ba03 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -121,7 +121,15 @@ namespace Avalonia.Controls.Notifications } } - /// + /// + /// Shows a Notification + /// + /// the content of the notification + /// the type of the notification + /// the expiration time of the notification after which it will automatically close. If the value is Zero then the notification will remain open until the user closes it + /// an Action to be run when the notification is clicked + /// an Action to be run when the notification is closed + /// style classes to apply public async void Show(object content, NotificationType type, TimeSpan? expiration = null, From 8b8b137c30af56285c18acdfdf507f2003b7aea0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 30 Aug 2023 17:46:12 -0700 Subject: [PATCH 09/13] Remove "Show Native Notification" as we won't have them soon --- samples/ControlCatalog/Pages/NotificationsPage.xaml | 3 +-- .../ControlCatalog/ViewModels/NotificationViewModel.cs | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/samples/ControlCatalog/Pages/NotificationsPage.xaml b/samples/ControlCatalog/Pages/NotificationsPage.xaml index 46c1fe52de..425837d4b9 100644 --- a/samples/ControlCatalog/Pages/NotificationsPage.xaml +++ b/samples/ControlCatalog/Pages/NotificationsPage.xaml @@ -6,6 +6,5 @@ + + + + diff --git a/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs b/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs index 6f83e5c366..fac1989976 100644 --- a/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Notifications; using Avalonia.Markup.Xaml; using ControlCatalog.ViewModels; @@ -27,7 +28,12 @@ namespace ControlCatalog.Pages { base.OnAttachedToVisualTree(e); - _viewModel.NotificationManager = new Avalonia.Controls.Notifications.WindowNotificationManager(TopLevel.GetTopLevel(this)); + _viewModel.NotificationManager = new WindowNotificationManager(TopLevel.GetTopLevel(this)!); + } + + public void NotificationOnClick() + { + this.Get("ControlNotifications").Show("Notification clicked"); } } }