完成 Message 控件

This commit is contained in:
polarboy 2024-09-02 10:43:16 +08:00
parent 54b5d5e289
commit 12e06ef532
18 changed files with 960 additions and 33 deletions

View File

@ -8,6 +8,54 @@
xmlns:showcase="clr-namespace:AtomUI.Demo.Desktop.ShowCase"
mc:Ignorable="d">
<showcase:ShowCasePanel>
<showcase:ShowCaseItem
Title="Basic"
Description="The simplest usage for Message.">
<atom:Button ButtonType="Primary"
Click="ShowSimpleMessage">
Display normal message
</atom:Button>
</showcase:ShowCaseItem>
<showcase:ShowCaseItem
Title="Other types of message"
Description="Messages of success, error and warning types.">
<StackPanel Orientation="Horizontal" Spacing="10">
<atom:Button ButtonType="Default"
Click="ShowSuccessMessage">
Success
</atom:Button>
<atom:Button ButtonType="Default"
Click="ShowInfoMessage">
Info
</atom:Button>
<atom:Button ButtonType="Default"
Click="ShowWarningMessage">
Warning
</atom:Button>
<atom:Button ButtonType="Default"
Click="ShowErrorMessage">
Error
</atom:Button>
</StackPanel>
</showcase:ShowCaseItem>
<showcase:ShowCaseItem
Title="Message with loading indicator"
Description="Display a global loading indicator, which is dismissed by itself asynchronously.">
<atom:Button ButtonType="Default"
Click="ShowLoadingMessage">
Display a loading indicator
</atom:Button>
</showcase:ShowCaseItem>
<showcase:ShowCaseItem
Title="Callback"
Description="The above example will display a new message when the old message is about to close.">
<atom:Button ButtonType="Default"
Click="ShowSequentialMessage">
Display a loading indicator
</atom:Button>
</showcase:ShowCaseItem>
</showcase:ShowCasePanel>
</UserControl>

View File

@ -1,11 +1,98 @@
using Avalonia.Controls;
using AtomUI.Controls.Message;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace AtomUI.Demo.Desktop.ShowCase;
public partial class MessageShowCase : UserControl
{
private WindowMessageManager? _messageManager;
public MessageShowCase()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var topLevel = TopLevel.GetTopLevel(this);
_messageManager = new WindowMessageManager(topLevel)
{
MaxItems = 10
};
}
private void ShowSimpleMessage(object? sender, RoutedEventArgs e)
{
_messageManager?.Show(new Message(
content: "Hello, AtomUI/Avalonia!"
));
}
private void ShowInfoMessage(object? sender, RoutedEventArgs e)
{
_messageManager?.Show(new Message(
type: MessageType.Information,
content: "This is a information message."
));
}
private void ShowSuccessMessage(object? sender, RoutedEventArgs e)
{
_messageManager?.Show(new Message(
type: MessageType.Success,
content: "This is a success message."
));
}
private void ShowWarningMessage(object? sender, RoutedEventArgs e)
{
_messageManager?.Show(new Message(
type: MessageType.Warning,
content: "This is a warning message."
));
}
private void ShowErrorMessage(object? sender, RoutedEventArgs e)
{
_messageManager?.Show(new Message(
type: MessageType.Error,
content: "This is a error message."
));
}
private void ShowLoadingMessage(object? sender, RoutedEventArgs e)
{
_messageManager?.Show(new Message(
type: MessageType.Loading,
content: "Action in progress..."
));
}
private void ShowSequentialMessage(object? sender, RoutedEventArgs e)
{
_messageManager?.Show(new Message(
type: MessageType.Loading,
content: "Action in progress...",
expiration: TimeSpan.FromSeconds(2.5),
onClose: () =>
{
_messageManager?.Show(new Message(
type: MessageType.Success,
expiration: TimeSpan.FromSeconds(2.5),
content: "Loading finished",
onClose: () =>
{
_messageManager?.Show(new Message(
type: MessageType.Information,
expiration: TimeSpan.FromSeconds(2.5),
content: "Loading finished"
));
}
));
}
));
}
}

View File

@ -43,6 +43,8 @@
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.MenuSeparatorTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.MenuTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.TopLevelMenuItemTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.Message.MessageCardTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.Message.WindowMessageManagerTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.NotificationCardTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.WindowNotificationManagerTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.NumericUpDownTheme());

View File

@ -22,6 +22,7 @@
ThemeManager.Current.RegisterControlTokenType(typeof(AtomUI.Controls.LoadingIndicatorToken));
ThemeManager.Current.RegisterControlTokenType(typeof(AtomUI.Controls.MarqueeLabelToken));
ThemeManager.Current.RegisterControlTokenType(typeof(AtomUI.Controls.MenuToken));
ThemeManager.Current.RegisterControlTokenType(typeof(AtomUI.Controls.Message.MessageToken));
ThemeManager.Current.RegisterControlTokenType(typeof(AtomUI.Controls.NotificationToken));
ThemeManager.Current.RegisterControlTokenType(typeof(AtomUI.Controls.NumericUpDownToken));
ThemeManager.Current.RegisterControlTokenType(typeof(AtomUI.Controls.OptionButtonToken));

View File

@ -305,9 +305,18 @@ namespace AtomUI.Theme.Styling
public static readonly TokenResourceKey SeparatorItemHeight = new TokenResourceKey("Menu.SeparatorItemHeight", "AtomUI.Token");
}
public static class MessageTokenResourceKey
{
public static readonly TokenResourceKey ContentBg = new TokenResourceKey("Message.ContentBg", "AtomUI.Token");
public static readonly TokenResourceKey ContentPadding = new TokenResourceKey("Message.ContentPadding", "AtomUI.Token");
public static readonly TokenResourceKey CardHeight = new TokenResourceKey("Message.CardHeight", "AtomUI.Token");
public static readonly TokenResourceKey MessageIconSize = new TokenResourceKey("Message.MessageIconSize", "AtomUI.Token");
public static readonly TokenResourceKey MessageIconMargin = new TokenResourceKey("Message.MessageIconMargin", "AtomUI.Token");
public static readonly TokenResourceKey MessageTopMargin = new TokenResourceKey("Message.MessageTopMargin", "AtomUI.Token");
}
public static class NotificationTokenResourceKey
{
public static readonly TokenResourceKey AnimationMaxHeight = new TokenResourceKey("Notification.AnimationMaxHeight", "AtomUI.Token");
public static readonly TokenResourceKey NotificationBg = new TokenResourceKey("Notification.NotificationBg", "AtomUI.Token");
public static readonly TokenResourceKey NotificationPadding = new TokenResourceKey("Notification.NotificationPadding", "AtomUI.Token");
public static readonly TokenResourceKey NotificationIconSize = new TokenResourceKey("Notification.NotificationIconSize", "AtomUI.Token");

View File

@ -0,0 +1,30 @@
namespace AtomUI.Controls.Message;
public interface IMessage
{
/// <summary>
/// Gets the message.
/// </summary>
string Content { get; }
/// <summary>
/// 自定义图标
/// </summary>
PathIcon? Icon { get; }
/// <summary>
/// Gets the <see cref="MessageType"/> of the notification.
/// </summary>
MessageType Type { get; }
/// <summary>
/// Gets the expiration time of the notification after which it will automatically close.
/// If the value is <see cref="TimeSpan.Zero"/> then the notification will remain open until the user closes it.
/// </summary>
TimeSpan Expiration { get; }
/// <summary>
/// Gets an Action to be run when the message is closed.
/// </summary>
Action? OnClose { get; }
}

View File

@ -0,0 +1,6 @@
namespace AtomUI.Controls.Message;
public interface IMessageManager
{
public void Show(IMessage message, string[]? classes = null);
}

View File

@ -0,0 +1,60 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace AtomUI.Controls.Message;
public class Message : IMessage, INotifyPropertyChanged
{
private string _content;
private PathIcon? _icon;
public Message(string content,
MessageType type = MessageType.Information,
PathIcon? icon = null,
TimeSpan? expiration = null,
Action? onClose = null)
{
_content = content;
_icon = icon;
Type = type;
Expiration = expiration.HasValue ? expiration.Value : TimeSpan.FromSeconds(5);
OnClose = onClose;
}
public string Content
{
get => _content;
set
{
if (_content != value) {
_content = value;
OnPropertyChanged();
}
}
}
public PathIcon? Icon
{
get => _icon;
set
{
if (!object.ReferenceEquals(_icon, value)) {
_icon = value;
OnPropertyChanged();
}
}
}
public MessageType Type { get; set; }
public TimeSpan Expiration { get; set; }
public Action? OnClose { get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View File

@ -0,0 +1,235 @@
using AtomUI.Icon;
using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
namespace AtomUI.Controls.Message;
[PseudoClasses(ErrorPC, InformationPC, SuccessPC, WarningPC, LoadingPC)]
public class MessageCard : TemplatedControl
{
public const string ErrorPC = ":error";
public const string InformationPC = ":information";
public const string SuccessPC = ":success";
public const string WarningPC = ":warning";
public const string LoadingPC = ":loading";
#region
/// <summary>
/// Defines the <see cref="IsClosing"/> property.
/// </summary>
public static readonly DirectProperty<MessageCard, bool> IsClosingProperty =
AvaloniaProperty.RegisterDirect<MessageCard, bool>(nameof(IsClosing), o => o.IsClosing);
/// <summary>
/// Defines the <see cref="IsClosed"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsClosedProperty =
AvaloniaProperty.Register<MessageCard, bool>(nameof(IsClosed));
/// <summary>
/// Defines the <see cref="NotificationType" /> property
/// </summary>
public static readonly StyledProperty<MessageType> MessageTypeProperty =
AvaloniaProperty.Register<MessageCard, MessageType>(nameof(NotificationType));
/// <summary>
/// Defines the <see cref="MessageClosed"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> MessageClosedEvent =
RoutedEvent.Register<MessageCard, RoutedEventArgs>(nameof(MessageClosed), RoutingStrategies.Bubble);
public static readonly StyledProperty<PathIcon?> IconProperty
= AvaloniaProperty.Register<MessageCard, PathIcon?>(nameof(Icon));
public static readonly StyledProperty<string> MessageProperty =
AvaloniaProperty.Register<NotificationCard, string>(nameof(Message));
/// <summary>
/// Determines if the notification is already closing.
/// </summary>
public bool IsClosing
{
get => _isClosing;
private set => SetAndRaise(IsClosingProperty, ref _isClosing, value);
}
/// <summary>
/// Determines if the notification is closed.
/// </summary>
public bool IsClosed
{
get => GetValue(IsClosedProperty);
set => SetValue(IsClosedProperty, value);
}
/// <summary>
/// Gets or sets the type of the notification
/// </summary>
public MessageType MessageType
{
get => GetValue(MessageTypeProperty);
set => SetValue(MessageTypeProperty, value);
}
/// <summary>
/// Raised when the <see cref="MessageCard"/> has closed.
/// </summary>
public event EventHandler<RoutedEventArgs>? MessageClosed
{
add => AddHandler(MessageClosedEvent, value);
remove => RemoveHandler(MessageClosedEvent, value);
}
public PathIcon? Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public string Message
{
get => GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}
#endregion
private bool _isClosing;
/// <summary>
/// Initializes a new instance of the <see cref="MessageCard"/> class.
/// </summary>
public MessageCard()
{
UpdateMessageType();
ClipToBounds = false;
}
/// <summary>
/// Closes the <see cref="MessageCard"/>.
/// </summary>
public void Close()
{
if (IsClosing) {
return;
}
IsClosing = true;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == MessageTypeProperty) {
SetupMessageIcon();
UpdateMessageType();
}
if (e.Property == MessageTypeProperty) {
UpdateMessageType();
}
if (e.Property == IsClosedProperty) {
if (!IsClosing && !IsClosed) {
return;
}
RaiseEvent(new RoutedEventArgs(MessageClosedEvent));
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (Icon is null) {
SetupMessageIcon();
UpdateMessageType();
}
}
private void UpdateMessageType()
{
switch (MessageType) {
case MessageType.Error:
PseudoClasses.Add(":error");
break;
case MessageType.Information:
PseudoClasses.Add(":information");
break;
case MessageType.Success:
PseudoClasses.Add(":success");
break;
case MessageType.Warning:
PseudoClasses.Add(":warning");
break;
case MessageType.Loading:
PseudoClasses.Add(":loading");
break;
}
if (Icon is not null) {
SetupMessageIconColor(Icon);
}
}
private void SetupMessageIconColor(PathIcon icon)
{
if (MessageType == MessageType.Error) {
TokenResourceBinder.CreateGlobalTokenBinding(icon, PathIcon.NormalFilledBrushProperty, GlobalTokenResourceKey.ColorError);
} else if (MessageType == MessageType.Information ||
MessageType == MessageType.Loading) {
TokenResourceBinder.CreateGlobalTokenBinding(icon, PathIcon.NormalFilledBrushProperty, GlobalTokenResourceKey.ColorPrimary);
} else if (MessageType == MessageType.Success) {
TokenResourceBinder.CreateGlobalTokenBinding(icon, PathIcon.NormalFilledBrushProperty, GlobalTokenResourceKey.ColorSuccess);
} else if (MessageType == MessageType.Warning) {
TokenResourceBinder.CreateGlobalTokenBinding(icon, PathIcon.NormalFilledBrushProperty, GlobalTokenResourceKey.ColorWarning);
}
}
private void SetupMessageIcon()
{
PathIcon? icon = null;
if (MessageType == MessageType.Information) {
icon = new PathIcon()
{
Kind = "InfoCircleFilled"
};
} else if (MessageType == MessageType.Success) {
icon = new PathIcon()
{
Kind = "CheckCircleFilled"
};
} else if (MessageType == MessageType.Error) {
icon = new PathIcon()
{
Kind = "CloseCircleFilled"
};
} else if (MessageType == MessageType.Warning) {
icon = new PathIcon()
{
Kind = "ExclamationCircleFilled"
};
} else if (MessageType == MessageType.Loading) {
icon = new PathIcon()
{
Kind = "LoadingOutlined",
LoadingAnimation = IconAnimation.Spin
};
}
if (icon is not null) {
SetupMessageIconColor(icon);
}
SetCurrentValue(IconProperty, icon);
}
}

View File

@ -0,0 +1,212 @@
using AtomUI.Controls.Utils;
using AtomUI.Theme;
using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Layout;
using Avalonia.Styling;
namespace AtomUI.Controls.Message;
[ControlThemeProvider]
internal class MessageCardTheme : BaseControlTheme
{
public const string FrameDecoratorPart = "PART_FrameDecorator";
public const string IconContentPart = "PART_IconContent";
public const string HeaderContainerPart = "PART_HeaderContainer";
public const string MessagePart = "PART_Message";
public const string LayoutTransformControlPart = "PART_LayoutTransformControl";
public const string MarginGhostDecoratorPart = "PART_MarginGhostDecorator";
public const double AnimationMaxOffsetY = 100d;
public const int AnimationDuration = 400;
private Easing _quadraticEaseOut = new QuadraticEaseOut();
private Easing _quadraticEaseIn = new QuadraticEaseIn();
public MessageCardTheme()
: base(typeof(MessageCard)) { }
protected override IControlTemplate BuildControlTemplate()
{
return new FuncControlTemplate<MessageCard>((card, scope) =>
{
BuildInstanceStyles(card);
var layoutTransformControl = new LayoutTransformControl()
{
Name = LayoutTransformControlPart,
ClipToBounds = false
};
layoutTransformControl.RegisterInNameScope(scope);
// 防止关闭的时候抖动,如果直接设置到 MessageCardlayoutTransformControl没有办法平滑处理
var marginGhostDecorator = new Border()
{
Name = MarginGhostDecoratorPart,
ClipToBounds = false
};
var frameDecorator = new Border()
{
Name = FrameDecoratorPart,
};
marginGhostDecorator.Child = frameDecorator;
var header = BuildContent(scope);
frameDecorator.Child = header;
frameDecorator.RegisterInNameScope(scope);
layoutTransformControl.Child = marginGhostDecorator;
return layoutTransformControl;
});
}
private DockPanel BuildContent(INameScope scope)
{
var headerLayout = new DockPanel()
{
Name = HeaderContainerPart,
LastChildFill = true
};
var iconContent = new ContentPresenter()
{
Name = IconContentPart
};
DockPanel.SetDock(iconContent, Dock.Left);
CreateTemplateParentBinding(iconContent, ContentPresenter.IsVisibleProperty, MessageCard.IconProperty,
BindingMode.Default,
ObjectConverters.IsNotNull);
CreateTemplateParentBinding(iconContent, ContentPresenter.ContentProperty, MessageCard.IconProperty);
TokenResourceBinder.CreateTokenBinding(iconContent, ContentPresenter.MarginProperty, MessageTokenResourceKey.MessageIconMargin);
headerLayout.Children.Add(iconContent);
var message = new SelectableTextBlock()
{
Name = MessagePart
};
TokenResourceBinder.CreateGlobalTokenBinding(message, SelectableTextBlock.SelectionBrushProperty,
GlobalTokenResourceKey.SelectionBackground);
TokenResourceBinder.CreateGlobalTokenBinding(message, SelectableTextBlock.SelectionForegroundBrushProperty,
GlobalTokenResourceKey.SelectionForeground);
CreateTemplateParentBinding(message, SelectableTextBlock.TextProperty, MessageCard.MessageProperty);
headerLayout.Children.Add(message);
return headerLayout;
}
protected override void BuildStyles()
{
BuildCommonStyle();
BuildContentStyle();
BuildContentStyle();
BuildAnimationStyle();
}
private void BuildCommonStyle()
{
var commonStyle = new Style(selector => selector.Nesting());
commonStyle.Add(MessageCard.HorizontalAlignmentProperty, HorizontalAlignment.Center);
var marginGhostDecoratorStyle =
new Style(selector => selector.Nesting().Template().Name(MarginGhostDecoratorPart));
marginGhostDecoratorStyle.Add(Border.MarginProperty, MessageTokenResourceKey.MessageTopMargin);
commonStyle.Add(marginGhostDecoratorStyle);
var frameDecoratorStyle = new Style(selector => selector.Nesting().Template().Name(FrameDecoratorPart));
frameDecoratorStyle.Add(Border.PaddingProperty, MessageTokenResourceKey.ContentPadding);
frameDecoratorStyle.Add(Border.BoxShadowProperty, GlobalTokenResourceKey.BoxShadows);
frameDecoratorStyle.Add(Border.BackgroundProperty, MessageTokenResourceKey.ContentBg);
frameDecoratorStyle.Add(Border.CornerRadiusProperty, GlobalTokenResourceKey.BorderRadiusLG);
commonStyle.Add(frameDecoratorStyle);
var closedStyle =
new Style(selector => selector.Nesting().PropertyEquals(MessageCard.IsClosedProperty, true));
closedStyle.Add(MessageCard.MarginProperty, new Thickness(0));
commonStyle.Add(closedStyle);
Add(commonStyle);
}
private void BuildContentStyle()
{
var titleStyle = new Style(selector => selector.Nesting().Template().Name(MessagePart));
titleStyle.Add(ContentPresenter.LineHeightProperty, GlobalTokenResourceKey.FontHeight);
titleStyle.Add(ContentPresenter.FontSizeProperty, GlobalTokenResourceKey.FontSize);
titleStyle.Add(ContentPresenter.ForegroundProperty, GlobalTokenResourceKey.ColorText);
Add(titleStyle);
}
protected override void BuildInstanceStyles(Control control)
{
var iconStyle = new Style(selector => selector.Name(IconContentPart).Child().OfType<PathIcon>());
iconStyle.Add(PathIcon.WidthProperty, MessageTokenResourceKey.MessageIconSize);
iconStyle.Add(PathIcon.HeightProperty, MessageTokenResourceKey.MessageIconSize);
control.Styles.Add(iconStyle);
}
private void BuildAnimationStyle()
{
var commonStyle = new Style(selector => selector.Nesting());
{
var layoutTransformStyle =
new Style(selector => selector.Nesting().Template().Name(LayoutTransformControlPart));
var moveRightInMotionConfig = MotionFactory.BuildMoveUpInMotion(
AnimationMaxOffsetY, TimeSpan.FromMilliseconds(AnimationDuration), _quadraticEaseOut,
FillMode.Forward);
foreach (var animation in moveRightInMotionConfig.Animations) {
layoutTransformStyle.Animations.Add(animation);
}
commonStyle.Add(layoutTransformStyle);
}
var isClosingStyle =
new Style(selector => selector.Nesting().PropertyEquals(MessageCard.IsClosingProperty, true));
{
var layoutTransformStyle =
new Style(selector => selector.Nesting().Template().Name(LayoutTransformControlPart));
var moveRightOutMotionConfig = MotionFactory.BuildMoveUpOutMotion(
AnimationMaxOffsetY, TimeSpan.FromMilliseconds(AnimationDuration), _quadraticEaseIn, FillMode.Forward);
foreach (var animation in moveRightOutMotionConfig.Animations) {
layoutTransformStyle.Animations.Add(animation);
}
isClosingStyle.Animations.Add(new Animation()
{
Duration = TimeSpan.FromMilliseconds(AnimationDuration * 1.2),
Easing = _quadraticEaseIn,
FillMode = FillMode.Forward,
Children =
{
new KeyFrame()
{
Cue = new Cue(1.0),
Setters =
{
new Setter(MessageCard.IsClosedProperty, true)
}
}
}
});
isClosingStyle.Add(layoutTransformStyle);
}
commonStyle.Add(isClosingStyle);
Add(commonStyle);
}
}

View File

@ -0,0 +1,57 @@
using AtomUI.Theme.TokenSystem;
using Avalonia;
using Avalonia.Media;
namespace AtomUI.Controls.Message;
[ControlDesignToken]
public class MessageToken : AbstractControlDesignToken
{
public const string ID = "Message";
/// <summary>
/// 提示框背景色
/// </summary>
public Color ContentBg { get; set; }
/// <summary>
/// 提示框内边距
/// </summary>
public Thickness ContentPadding { get; set; }
/// <summary>
/// 提示框高度
/// </summary>
public double CardHeight { get; set; }
/// <summary>
/// 提醒框图标尺寸
/// </summary>
public double MessageIconSize { get; set; }
/// <summary>
/// 提醒框图标外边距
/// </summary>
public Thickness MessageIconMargin { get; set; }
/// <summary>
/// 提醒框上边缘外边距
/// </summary>
public Thickness MessageTopMargin { get; set; }
public MessageToken()
: base(ID)
{
}
internal override void CalculateFromAlias()
{
base.CalculateFromAlias();
ContentBg = _globalToken.ColorToken.ColorNeutralToken.ColorBgElevated;
ContentPadding = new Thickness((_globalToken.HeightToken.ControlHeightLG - _globalToken.FontToken.FontSize * _globalToken.FontToken.LineHeight) / 2,
_globalToken.PaddingXS);
MessageIconMargin = new Thickness(0, 0, _globalToken.MarginXS, 0);
MessageTopMargin = new Thickness(_globalToken.Margin, _globalToken.Margin, _globalToken.Margin, 0);
MessageIconSize = _globalToken.FontToken.FontSizeSM * _globalToken.FontToken.LineHeightSM;
}
}

View File

@ -0,0 +1,10 @@
namespace AtomUI.Controls.Message;
public enum MessageType
{
Information,
Success,
Warning,
Error,
Loading
}

View File

@ -0,0 +1,149 @@
using System.Collections;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace AtomUI.Controls.Message;
[TemplatePart(WindowNotificationManagerTheme.ItemsPart, typeof(Panel))]
public class WindowMessageManager : TemplatedControl, IMessageManager
{
private IList? _items;
/// <summary>
/// Defines the <see cref="Position"/> property.
/// </summary>
public static readonly StyledProperty<NotificationPosition> PositionProperty =
AvaloniaProperty.Register<WindowNotificationManager, NotificationPosition>(
nameof(Position), NotificationPosition.TopRight);
/// <summary>
/// Defines which corner of the screen notifications can be displayed in.
/// </summary>
/// <seealso cref="NotificationPosition"/>
public NotificationPosition Position
{
get => GetValue(PositionProperty);
set => SetValue(PositionProperty, value);
}
/// <summary>
/// Defines the <see cref="MaxItems"/> property.
/// </summary>
public static readonly StyledProperty<int> MaxItemsProperty =
AvaloniaProperty.Register<WindowNotificationManager, int>(nameof(MaxItems), 5);
/// <summary>
/// Defines the maximum number of notifications visible at once.
/// </summary>
public int MaxItems
{
get => GetValue(MaxItemsProperty);
set => SetValue(MaxItemsProperty, value);
}
/// <summary>
/// Initializes a new instance of the <see cref="WindowNotificationManager"/> class.
/// </summary>
/// <param name="host">The TopLevel that will host the control.</param>
public WindowMessageManager(TopLevel? host)
{
if (host is not null) {
InstallFromTopLevel(host);
}
}
static WindowMessageManager()
{
HorizontalAlignmentProperty.OverrideDefaultValue<WindowMessageManager>(HorizontalAlignment.Stretch);
VerticalAlignmentProperty.OverrideDefaultValue<WindowMessageManager>(VerticalAlignment.Stretch);
}
/// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var itemsControl = e.NameScope.Find<Panel>("PART_Items");
_items = itemsControl?.Children;
}
/// <summary>
/// Shows a Notification
/// </summary>
/// <param name="message">the content of the message</param>
/// <param name="classes">style classes to apply</param>
public async void Show(IMessage message, string[]? classes = null)
{
var expiration = message.Expiration;
var onClose = message.OnClose;
Dispatcher.UIThread.VerifyAccess();
var messageControl = new MessageCard
{
Icon = message.Icon,
Message = message.Content,
MessageType = message.Type
};
// Add style classes if any
if (classes != null) {
foreach (var @class in classes) {
messageControl.Classes.Add(@class);
}
}
messageControl.MessageClosed += (sender, args) =>
{
onClose?.Invoke();
_items?.Remove(sender);
};
Dispatcher.UIThread.Post(() =>
{
_items?.Add(messageControl);
if (_items?.OfType<MessageCard>().Count(i => !i.IsClosing) > MaxItems) {
_items.OfType<MessageCard>().First(i => !i.IsClosing).Close();
}
});
if (expiration == TimeSpan.Zero) {
return;
}
await Task.Delay(expiration);
messageControl.Close();
}
/// <summary>
/// Installs the <see cref="WindowNotificationManager"/> within the <see cref="AdornerLayer"/>
/// </summary>
private void InstallFromTopLevel(TopLevel topLevel)
{
topLevel.TemplateApplied += TopLevelOnTemplateApplied;
var adorner = topLevel.FindDescendantOfType<VisualLayerManager>()?.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 notification manager on template reapplied.
var topLevel = (TopLevel)sender!;
topLevel.TemplateApplied -= TopLevelOnTemplateApplied;
InstallFromTopLevel(topLevel);
}
}

View File

@ -0,0 +1,44 @@
using AtomUI.Theme;
using AtomUI.Theme.Styling;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Styling;
namespace AtomUI.Controls.Message;
[ControlThemeProvider]
internal class WindowMessageManagerTheme : BaseControlTheme
{
public const string ItemsPart = "PART_Items";
public WindowMessageManagerTheme() : base(typeof(WindowMessageManager))
{
}
protected override IControlTemplate BuildControlTemplate()
{
return new FuncControlTemplate<WindowMessageManager>((manager, scope) =>
{
var mainLayout = new ReversibleStackPanel()
{
Name = ItemsPart,
};
mainLayout.RegisterInNameScope(scope);
return mainLayout;
});
}
protected override void BuildStyles()
{
var topCenterStyle = new Style(selector => selector.Nesting().Class(WindowNotificationManager.TopCenterPC));
{
var itemsStyle = new Style(selector => selector.Nesting().Template().Name(ItemsPart));
itemsStyle.Add(ReversibleStackPanel.ReverseOrderProperty, true);
itemsStyle.Add(ReversibleStackPanel.HorizontalAlignmentProperty, HorizontalAlignment.Center);
itemsStyle.Add(ReversibleStackPanel.VerticalAlignmentProperty, VerticalAlignment.Top);
topCenterStyle.Add(itemsStyle);
}
Add(topCenterStyle);
}
}

View File

@ -13,7 +13,7 @@ using Avalonia.LogicalTree;
namespace AtomUI.Controls;
[PseudoClasses(ErrorPC, InformationPC, SuccessPC, WarningPC)]
public class NotificationCard : TemplatedControl
public class NotificationCard : ContentControl
{
public const string ErrorPC = ":error";
public const string InformationPC = ":information";
@ -52,12 +52,6 @@ public class NotificationCard : TemplatedControl
public static readonly StyledProperty<string> TitleProperty =
AvaloniaProperty.Register<NotificationCard, string>(nameof(Title));
public static readonly StyledProperty<object?> CardContentProperty =
AvaloniaProperty.Register<NotificationCard, object?>(nameof(CardContent));
public static readonly StyledProperty<IDataTemplate?> CardContentTemplateProperty =
AvaloniaProperty.Register<NotificationCard, IDataTemplate?>(nameof(CardContentTemplate));
public static readonly StyledProperty<PathIcon?> IconProperty
= AvaloniaProperty.Register<NotificationCard, PathIcon?>(nameof(Icon));
@ -100,18 +94,6 @@ public class NotificationCard : TemplatedControl
set => SetValue(TitleProperty, value);
}
public object? CardContent
{
get => GetValue(CardContentProperty);
set => SetValue(CardContentProperty, value);
}
public object? CardContentTemplate
{
get => GetValue(CardContentTemplateProperty);
set => SetValue(CardContentTemplateProperty, value);
}
public PathIcon? Icon
{
get => GetValue(IconProperty);
@ -242,7 +224,7 @@ public class NotificationCard : TemplatedControl
RaiseEvent(new RoutedEventArgs(NotificationClosedEvent));
}
if (e.Property == CardContentProperty) {
if (e.Property == ContentProperty) {
if (e.NewValue is string) {
SetupContent();
}
@ -311,14 +293,14 @@ public class NotificationCard : TemplatedControl
private void SetupContent()
{
if (CardContent is string content) {
if (Content is string content) {
var textBlock = new SelectableTextBlock()
{
Text = content,
};
TokenResourceBinder.CreateGlobalTokenBinding(textBlock, SelectableTextBlock.SelectionBrushProperty, GlobalTokenResourceKey.SelectionBackground);
TokenResourceBinder.CreateGlobalTokenBinding(textBlock, SelectableTextBlock.SelectionForegroundBrushProperty, GlobalTokenResourceKey.SelectionForeground);
CardContent = textBlock;
Content = textBlock;
}
}

View File

@ -174,9 +174,9 @@ internal class NotificationCardTheme : BaseControlTheme
};
TokenResourceBinder.CreateTokenBinding(contentPresenter, ContentPresenter.MarginProperty, NotificationTokenResourceKey.NotificationContentMargin);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ContentProperty,
NotificationCard.CardContentProperty);
NotificationCard.ContentProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ContentTemplateProperty,
NotificationCard.CardContentTemplateProperty);
NotificationCard.ContentTemplateProperty);
Grid.SetColumn(contentPresenter, 1);
Grid.SetRow(contentPresenter, 1);
layout.Children.Add(contentPresenter);

View File

@ -14,11 +14,6 @@ internal class NotificationToken : AbstractControlDesignToken
{
}
/// <summary>
/// 动画最大高度
/// </summary>
public double AnimationMaxHeight { get; set; }
/// <summary>
/// 提醒框背景色
/// </summary>

View File

@ -143,7 +143,7 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager
var notificationControl = new NotificationCard(this)
{
Title = notification.Title,
CardContent = notification.Content,
Content = notification.Content,
Icon = notification.Icon,
NotificationType = notification.Type,
Expiration = expiration == TimeSpan.Zero ? null : expiration,