diff --git a/src/Ursa.Themes.Semi/Controls/Toast.axaml b/src/Ursa.Themes.Semi/Controls/Toast.axaml
index 256140b..d34388b 100644
--- a/src/Ursa.Themes.Semi/Controls/Toast.axaml
+++ b/src/Ursa.Themes.Semi/Controls/Toast.axaml
@@ -6,10 +6,10 @@
- Hello, Semi.Avalonia!
+ Hello, Ursa!
-
+
@@ -87,7 +87,7 @@
Grid.Column="2"
Theme="{StaticResource ToastCloseButton}"
IsVisible="{TemplateBinding ShowClose}"
- u:ToastCard.CloseOnClick="True" />
+ u:MessageCard.CloseOnClick="True" />
diff --git a/src/Ursa/Controls/NotificationShared/IMessage.cs b/src/Ursa/Controls/NotificationShared/IMessage.cs
new file mode 100644
index 0000000..b1cad33
--- /dev/null
+++ b/src/Ursa/Controls/NotificationShared/IMessage.cs
@@ -0,0 +1,35 @@
+using Avalonia.Controls.Notifications;
+
+namespace Ursa.Controls;
+
+///
+/// Represents a message that can be shown in a window or by the host operating system.
+///
+public interface IMessage
+{
+ ///
+ /// Gets the of the message.
+ ///
+ NotificationType Type { get; }
+
+ ///
+ /// Gets a value indicating whether the message should show a close button.
+ ///
+ bool ShowClose { get; }
+
+ ///
+ /// Gets the expiration time of the message after which it will automatically close.
+ /// If the value is then the message will remain open until the user closes it.
+ ///
+ TimeSpan Expiration { get; }
+
+ ///
+ /// Gets an Action to be run when the message is clicked.
+ ///
+ Action? OnClick { get; }
+
+ ///
+ /// Gets an Action to be run when the message is closed.
+ ///
+ Action? OnClose { get; }
+}
\ No newline at end of file
diff --git a/src/Ursa/Controls/NotificationShared/MessageCard.cs b/src/Ursa/Controls/NotificationShared/MessageCard.cs
new file mode 100644
index 0000000..d244a69
--- /dev/null
+++ b/src/Ursa/Controls/NotificationShared/MessageCard.cs
@@ -0,0 +1,207 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Notifications;
+using Avalonia.Interactivity;
+using Avalonia.LogicalTree;
+
+namespace Ursa.Controls;
+
+///
+/// Control that represents and displays a message.
+///
+[PseudoClasses(PC_Information, PC_Success, PC_Warning, PC_Error)]
+public abstract class MessageCard : ContentControl
+{
+ public const string PC_Information = ":information";
+ public const string PC_Success = ":success";
+ public const string PC_Warning = ":warning";
+ public const string PC_Error = ":error";
+
+ private bool _isClosing;
+
+ static MessageCard()
+ {
+ CloseOnClickProperty.Changed.AddClassHandler(OnCloseOnClickPropertyChanged);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MessageCard()
+ {
+ UpdateNotificationType();
+ }
+
+ ///
+ /// Determines if the message is already closing.
+ ///
+ public bool IsClosing
+ {
+ get => _isClosing;
+ private set => SetAndRaise(IsClosingProperty, ref _isClosing, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty IsClosingProperty =
+ AvaloniaProperty.RegisterDirect(nameof(IsClosing), o => o.IsClosing);
+
+ ///
+ /// Determines if the message is closed.
+ ///
+ public bool IsClosed
+ {
+ get => GetValue(IsClosedProperty);
+ set => SetValue(IsClosedProperty, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsClosedProperty =
+ AvaloniaProperty.Register(nameof(IsClosed));
+
+ ///
+ /// Gets or sets the type of the message
+ ///
+ public NotificationType NotificationType
+ {
+ get => GetValue(NotificationTypeProperty);
+ set => SetValue(NotificationTypeProperty, value);
+ }
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty NotificationTypeProperty =
+ AvaloniaProperty.Register(nameof(NotificationType));
+
+ public bool ShowClose
+ {
+ get => GetValue(ShowCloseProperty);
+ set => SetValue(ShowCloseProperty, value);
+ }
+
+ public static readonly StyledProperty ShowCloseProperty =
+ AvaloniaProperty.Register(nameof(ShowClose), true);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent MessageClosedEvent =
+ RoutedEvent.Register(nameof(MessageClosed), RoutingStrategies.Bubble);
+
+
+ ///
+ /// Raised when the has closed.
+ ///
+ public event EventHandler? MessageClosed
+ {
+ add => AddHandler(MessageClosedEvent, value);
+ remove => RemoveHandler(MessageClosedEvent, value);
+ }
+
+ public static bool GetCloseOnClick(Button obj)
+ {
+ _ = obj ?? throw new ArgumentNullException(nameof(obj));
+ return obj.GetValue(CloseOnClickProperty);
+ }
+
+ public static void SetCloseOnClick(Button obj, bool value)
+ {
+ _ = obj ?? throw new ArgumentNullException(nameof(obj));
+ obj.SetValue(CloseOnClickProperty, value);
+ }
+
+ ///
+ /// Defines the CloseOnClick property.
+ ///
+ public static readonly AttachedProperty CloseOnClickProperty =
+ AvaloniaProperty.RegisterAttached("CloseOnClick", defaultValue: false);
+
+ private static void OnCloseOnClickPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
+ {
+ var button = (Button)d;
+ var value = (bool)e.NewValue!;
+ if (value)
+ {
+ button.Click += Button_Click;
+ }
+ else
+ {
+ button.Click -= Button_Click;
+ }
+ }
+
+ ///
+ /// Called when a button inside the Message is clicked.
+ ///
+ private static void Button_Click(object? sender, RoutedEventArgs e)
+ {
+ var btn = sender as ILogical;
+ var message = btn?.GetLogicalAncestors().OfType().FirstOrDefault();
+ message?.Close();
+ }
+
+ ///
+ /// Closes the .
+ ///
+ public void Close()
+ {
+ if (IsClosing)
+ {
+ return;
+ }
+
+ IsClosing = true;
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ base.OnPropertyChanged(e);
+
+ if (e.Property == ContentProperty && e.NewValue is IMessage message)
+ {
+ SetValue(NotificationTypeProperty, message.Type);
+ }
+
+ if (e.Property == NotificationTypeProperty)
+ {
+ UpdateNotificationType();
+ }
+
+ if (e.Property == IsClosedProperty)
+ {
+ if (!IsClosing && !IsClosed)
+ {
+ return;
+ }
+
+ RaiseEvent(new RoutedEventArgs(MessageClosedEvent));
+ }
+ }
+
+ private void UpdateNotificationType()
+ {
+ switch (NotificationType)
+ {
+ case NotificationType.Error:
+ PseudoClasses.Add(PC_Error);
+ break;
+
+ case NotificationType.Information:
+ PseudoClasses.Add(PC_Information);
+ break;
+
+ case NotificationType.Success:
+ PseudoClasses.Add(PC_Success);
+ break;
+
+ case NotificationType.Warning:
+ PseudoClasses.Add(PC_Warning);
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ursa/Controls/NotificationShared/WindowMessageManager.cs b/src/Ursa/Controls/NotificationShared/WindowMessageManager.cs
new file mode 100644
index 0000000..e79e03e
--- /dev/null
+++ b/src/Ursa/Controls/NotificationShared/WindowMessageManager.cs
@@ -0,0 +1,80 @@
+using System.Collections;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
+using Avalonia.VisualTree;
+
+namespace Ursa.Controls;
+
+///
+/// An that displays messages in a .
+///
+[TemplatePart(PART_Items, typeof(Panel))]
+public abstract class WindowMessageManager : TemplatedControl
+{
+ public const string PART_Items = "PART_Items";
+
+ protected IList? _items;
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MaxItemsProperty =
+ AvaloniaProperty.Register(nameof(MaxItems), 5);
+
+ ///
+ /// Defines the maximum number of messages visible at once.
+ ///
+ public int MaxItems
+ {
+ get => GetValue(MaxItemsProperty);
+ set => SetValue(MaxItemsProperty, value);
+ }
+
+ static WindowMessageManager()
+ {
+ HorizontalAlignmentProperty.OverrideDefaultValue(HorizontalAlignment.Stretch);
+ VerticalAlignmentProperty.OverrideDefaultValue(VerticalAlignment.Stretch);
+ }
+
+ ///
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+
+ var itemsControl = e.NameScope.Find(PART_Items);
+ _items = itemsControl?.Children;
+ }
+
+ public abstract void Show(object content);
+
+ ///
+ /// Installs the within the
+ ///
+ protected void InstallFromTopLevel(TopLevel topLevel)
+ {
+ topLevel.TemplateApplied += TopLevelOnTemplateApplied;
+ var adorner = topLevel.FindDescendantOfType()?.AdornerLayer;
+ if (adorner is not null)
+ {
+ adorner.Children.Add(this);
+ AdornerLayer.SetAdornedElement(this, adorner);
+ }
+ }
+
+ protected void TopLevelOnTemplateApplied(object? sender, TemplateAppliedEventArgs e)
+ {
+ if (Parent is AdornerLayer adornerLayer)
+ {
+ adornerLayer.Children.Remove(this);
+ AdornerLayer.SetAdornedElement(this, null);
+ }
+
+ // Reinstall message manager on template reapplied.
+ var topLevel = (TopLevel)sender!;
+ topLevel.TemplateApplied -= TopLevelOnTemplateApplied;
+ InstallFromTopLevel(topLevel);
+ }
+}
\ No newline at end of file
diff --git a/src/Ursa/Controls/Toast/IManagedToastManager.cs b/src/Ursa/Controls/Toast/IManagedToastManager.cs
deleted file mode 100644
index 1a73d28..0000000
--- a/src/Ursa/Controls/Toast/IManagedToastManager.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using Avalonia.Metadata;
-
-namespace Ursa.Controls;
-
-///
-/// Represents a toast manager that can show arbitrary content.
-/// Managed toast managers can show any content.
-///
-///
-/// Because toast managers of this type are implemented purely in managed code, they
-/// can display arbitrary content, as opposed to toast managers which display toasts
-/// using the host operating system's toast mechanism.
-///
-[NotClientImplementable]
-public interface IManagedToastManager : IToastManager
-{
- ///
- /// Shows a toast.
- ///
- /// The content to be displayed.
- void Show(object content);
-}
\ No newline at end of file
diff --git a/src/Ursa/Controls/Toast/IToast.cs b/src/Ursa/Controls/Toast/IToast.cs
index 107a6d0..777fece 100644
--- a/src/Ursa/Controls/Toast/IToast.cs
+++ b/src/Ursa/Controls/Toast/IToast.cs
@@ -1,43 +1,12 @@
-using Avalonia.Controls.Notifications;
-using Avalonia.Metadata;
-
-namespace Ursa.Controls;
+namespace Ursa.Controls;
///
/// Represents a toast that can be shown in a window or by the host operating system.
///
-[NotClientImplementable]
-public interface IToast
+public interface IToast : IMessage
{
-
///
/// Gets the toast message.
///
string? Content { get; }
-
- ///
- /// Gets the of the toast.
- ///
- NotificationType Type { get; }
-
- ///
- /// Gets a value indicating whether the toast should show a close button.
- ///
- bool ShowClose { get; }
-
- ///
- /// Gets the expiration time of the toast after which it will automatically close.
- /// If the value is then the toast will remain open until the user closes it.
- ///
- TimeSpan Expiration { get; }
-
- ///
- /// Gets an Action to be run when the toast is clicked.
- ///
- Action? OnClick { get; }
-
- ///
- /// Gets an Action to be run when the toast is closed.
- ///
- Action? OnClose { get; }
}
\ No newline at end of file
diff --git a/src/Ursa/Controls/Toast/ToastCard.cs b/src/Ursa/Controls/Toast/ToastCard.cs
index b9084a3..8cd215a 100644
--- a/src/Ursa/Controls/Toast/ToastCard.cs
+++ b/src/Ursa/Controls/Toast/ToastCard.cs
@@ -1,206 +1,6 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Controls.Metadata;
-using Avalonia.Controls.Notifications;
-using Avalonia.Interactivity;
-using Avalonia.LogicalTree;
-
-namespace Ursa.Controls;
+namespace Ursa.Controls;
///
/// Control that represents and displays a toast.
///
-[PseudoClasses(PC_Information, PC_Success, PC_Warning, PC_Error)]
-public class ToastCard : ContentControl
-{
- public const string PC_Information = ":information";
- public const string PC_Success = ":success";
- public const string PC_Warning = ":warning";
- public const string PC_Error = ":error";
-
- private bool _isClosing;
-
- static ToastCard()
- {
- CloseOnClickProperty.Changed.AddClassHandler(OnCloseOnClickPropertyChanged);
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- public ToastCard()
- {
- UpdateNotificationType();
- }
-
- ///
- /// Determines if the toast is already closing.
- ///
- public bool IsClosing
- {
- get => _isClosing;
- private set => SetAndRaise(IsClosingProperty, ref _isClosing, value);
- }
-
- ///
- /// Defines the property.
- ///
- public static readonly DirectProperty IsClosingProperty =
- AvaloniaProperty.RegisterDirect(nameof(IsClosing), o => o.IsClosing);
-
- ///
- /// Determines if the toast is closed.
- ///
- public bool IsClosed
- {
- get => GetValue(IsClosedProperty);
- set => SetValue(IsClosedProperty, value);
- }
-
- ///
- /// Defines the property.
- ///
- public static readonly StyledProperty IsClosedProperty =
- AvaloniaProperty.Register(nameof(IsClosed));
-
- ///
- /// Gets or sets the type of the toast
- ///
- public NotificationType NotificationType
- {
- get => GetValue(NotificationTypeProperty);
- set => SetValue(NotificationTypeProperty, value);
- }
-
- ///
- /// Defines the property
- ///
- public static readonly StyledProperty NotificationTypeProperty =
- AvaloniaProperty.Register(nameof(NotificationType));
- public bool ShowClose
- {
- get => GetValue(ShowCloseProperty);
- set => SetValue(ShowCloseProperty, value);
- }
-
- public static readonly StyledProperty ShowCloseProperty =
- AvaloniaProperty.Register(nameof(ShowClose), true);
-
- ///
- /// Defines the event.
- ///
- public static readonly RoutedEvent ToastClosedEvent =
- RoutedEvent.Register(nameof(ToastClosed), RoutingStrategies.Bubble);
-
-
- ///
- /// Raised when the has closed.
- ///
- public event EventHandler? ToastClosed
- {
- add => AddHandler(ToastClosedEvent, value);
- remove => RemoveHandler(ToastClosedEvent, value);
- }
-
- public static bool GetCloseOnClick(Button obj)
- {
- _ = obj ?? throw new ArgumentNullException(nameof(obj));
- return obj.GetValue(CloseOnClickProperty);
- }
-
- public static void SetCloseOnClick(Button obj, bool value)
- {
- _ = obj ?? throw new ArgumentNullException(nameof(obj));
- obj.SetValue(CloseOnClickProperty, value);
- }
-
- ///
- /// Defines the CloseOnClick property.
- ///
- public static readonly AttachedProperty CloseOnClickProperty =
- AvaloniaProperty.RegisterAttached("CloseOnClick", defaultValue: false);
-
- private static void OnCloseOnClickPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
- {
- var button = (Button)d;
- var value = (bool)e.NewValue!;
- if (value)
- {
- button.Click += Button_Click;
- }
- else
- {
- button.Click -= Button_Click;
- }
- }
-
- ///
- /// Called when a button inside the Toast is clicked.
- ///
- private static void Button_Click(object? sender, RoutedEventArgs e)
- {
- var btn = sender as ILogical;
- var toast = btn?.GetLogicalAncestors().OfType().FirstOrDefault();
- toast?.Close();
- }
-
- ///
- /// Closes the .
- ///
- public void Close()
- {
- if (IsClosing)
- {
- return;
- }
-
- IsClosing = true;
- }
-
- protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
- {
- base.OnPropertyChanged(e);
-
- if (e.Property == ContentProperty && e.NewValue is IToast toast)
- {
- SetValue(NotificationTypeProperty, toast.Type);
- }
-
- if (e.Property == NotificationTypeProperty)
- {
- UpdateNotificationType();
- }
-
- if (e.Property == IsClosedProperty)
- {
- if (!IsClosing && !IsClosed)
- {
- return;
- }
-
- RaiseEvent(new RoutedEventArgs(ToastClosedEvent));
- }
- }
-
- 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;
- }
- }
-}
\ No newline at end of file
+public class ToastCard : MessageCard;
\ No newline at end of file
diff --git a/src/Ursa/Controls/Toast/WindowToastManager.cs b/src/Ursa/Controls/Toast/WindowToastManager.cs
index 85d492f..7ab3e15 100644
--- a/src/Ursa/Controls/Toast/WindowToastManager.cs
+++ b/src/Ursa/Controls/Toast/WindowToastManager.cs
@@ -1,38 +1,17 @@
-using System.Collections;
-using Avalonia;
-using Avalonia.Controls;
+using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Threading;
-using Avalonia.VisualTree;
namespace Ursa.Controls;
///
/// An that displays toasts in a .
///
-[TemplatePart("PART_Items", typeof(Panel))]
-public class WindowToastManager : TemplatedControl, IManagedToastManager
+public class WindowToastManager : WindowMessageManager, IToastManager
{
- private IList? _items;
-
- ///
- /// Defines the property.
- ///
- public static readonly StyledProperty MaxItemsProperty =
- AvaloniaProperty.Register(nameof(MaxItems), 5);
-
- ///
- /// Defines the maximum number of toasts visible at once.
- ///
- public int MaxItems
- {
- get => GetValue(MaxItemsProperty);
- set => SetValue(MaxItemsProperty, value);
- }
-
///
/// Initializes a new instance of the class.
///
@@ -52,21 +31,6 @@ public class WindowToastManager : TemplatedControl, IManagedToastManager
{
}
- static WindowToastManager()
- {
- HorizontalAlignmentProperty.OverrideDefaultValue(HorizontalAlignment.Stretch);
- VerticalAlignmentProperty.OverrideDefaultValue(VerticalAlignment.Stretch);
- }
-
- ///
- protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
- {
- base.OnApplyTemplate(e);
-
- var itemsControl = e.NameScope.Find("PART_Items");
- _items = itemsControl?.Children;
- }
-
///
public void Show(IToast content)
{
@@ -74,7 +38,7 @@ public class WindowToastManager : TemplatedControl, IManagedToastManager
}
///
- public void Show(object content)
+ public override void Show(object content)
{
if (content is IToast toast)
{
@@ -96,7 +60,8 @@ public class WindowToastManager : TemplatedControl, IManagedToastManager
/// an Action to be run when the toast is clicked
/// an Action to be run when the toast is closed
/// style classes to apply
- public async void Show(object content,
+ public async void Show(
+ object content,
NotificationType type,
TimeSpan? expiration = null,
bool showClose = true,
@@ -122,7 +87,7 @@ public class WindowToastManager : TemplatedControl, IManagedToastManager
}
}
- toastControl.ToastClosed += (sender, _) =>
+ toastControl.MessageClosed += (sender, _) =>
{
onClose?.Invoke();
@@ -150,32 +115,4 @@ public class WindowToastManager : TemplatedControl, IManagedToastManager
toastControl.Close();
}
-
- ///
- /// Installs the within the
- ///
- private void InstallFromTopLevel(TopLevel topLevel)
- {
- topLevel.TemplateApplied += TopLevelOnTemplateApplied;
- var adorner = topLevel.FindDescendantOfType()?.AdornerLayer;
- if (adorner is not null)
- {
- adorner.Children.Add(this);
- AdornerLayer.SetAdornedElement(this, adorner);
- }
- }
-
- private void TopLevelOnTemplateApplied(object? sender, TemplateAppliedEventArgs e)
- {
- if (Parent is AdornerLayer adornerLayer)
- {
- adornerLayer.Children.Remove(this);
- AdornerLayer.SetAdornedElement(this, null);
- }
-
- // Reinstall toast manager on template reapplied.
- var topLevel = (TopLevel)sender!;
- topLevel.TemplateApplied -= TopLevelOnTemplateApplied;
- InstallFromTopLevel(topLevel);
- }
}
\ No newline at end of file