mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-21 08:32:21 +08:00
feat(Message,Breadcrumb): port Message control and Breadcrumb showcase from release/5.0
This commit is contained in:
+2
-1
@@ -16,7 +16,8 @@ namespace AtomUIGallery.Localization
|
||||
General_SplitButton,
|
||||
Layout,
|
||||
Layout_Splitter,
|
||||
Navigation
|
||||
Navigation,
|
||||
Navigation_Breadcrumb
|
||||
}
|
||||
|
||||
public class CaseNavigationLangResourceExtension : LanguageResourceExtension<CaseNavigationLangResourceKind>
|
||||
|
||||
@@ -14,5 +14,6 @@ public sealed class ShowCaseViewModule : IViewModule
|
||||
locator.Map<ButtonViewModel, ButtonShowCase>(() => new ButtonShowCase());
|
||||
locator.Map<SplitButtonViewModel, SplitButtonShowCase>(() => new SplitButtonShowCase());
|
||||
locator.Map<SplitterViewModel, SplitterShowCase>(() => new SplitterShowCase());
|
||||
locator.Map<BreadcrumbViewModel, BreadcrumbShowCase>(() => new BreadcrumbShowCase());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using AtomUI.Controls;
|
||||
using AtomUI.Desktop.Controls;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace AtomUIGallery.ShowCases.ViewModels;
|
||||
|
||||
public class BreadcrumbViewModel : ReactiveObject, IRoutableViewModel, IActivatableViewModel
|
||||
{
|
||||
public static EntityKey ID = "Breadcrumb";
|
||||
|
||||
public IScreen HostScreen { get; }
|
||||
public ViewModelActivator Activator { get; }
|
||||
|
||||
public string? UrlPathSegment => ID.ToString();
|
||||
|
||||
private List<BreadcrumbItemData>? _breadcrumbItems = [];
|
||||
|
||||
public List<BreadcrumbItemData>? BreadcrumbItems
|
||||
{
|
||||
get => _breadcrumbItems;
|
||||
set => this.RaiseAndSetIfChanged(ref _breadcrumbItems, value);
|
||||
}
|
||||
|
||||
public BreadcrumbViewModel(IScreen screen)
|
||||
{
|
||||
Activator = new ViewModelActivator();
|
||||
HostScreen = screen;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="AtomUIGallery.ShowCases.Views.BreadcrumbShowCase"
|
||||
xmlns:atom="https://atomui.net"
|
||||
xmlns:antdicons="https://atomui.net/icons/antdesign"
|
||||
xmlns:gallery="https://atomui.net/oss-controls/gallery">
|
||||
<gallery:ShowCasePanel>
|
||||
<gallery:ShowCaseItem
|
||||
Title="Basic Usage"
|
||||
Description="The simplest use.">
|
||||
<StackPanel>
|
||||
<atom:Breadcrumb>
|
||||
<atom:BreadcrumbItem>Home</atom:BreadcrumbItem>
|
||||
<atom:BreadcrumbItem NavigateContext="#">Application Center</atom:BreadcrumbItem>
|
||||
<atom:BreadcrumbItem NavigateContext="#">Application List</atom:BreadcrumbItem>
|
||||
<atom:BreadcrumbItem>An Application</atom:BreadcrumbItem>
|
||||
</atom:Breadcrumb>
|
||||
</StackPanel>
|
||||
</gallery:ShowCaseItem>
|
||||
|
||||
<gallery:ShowCaseItem
|
||||
Title="With an Icon"
|
||||
Description="The icon should be placed in front of the text.">
|
||||
<StackPanel>
|
||||
<atom:Breadcrumb>
|
||||
<atom:BreadcrumbItem Icon="{antdicons:AntDesignIconProvider Kind=HomeOutlined}"></atom:BreadcrumbItem>
|
||||
<atom:BreadcrumbItem Icon="{antdicons:AntDesignIconProvider Kind=UserOutlined}" NavigateContext="#">Application List</atom:BreadcrumbItem>
|
||||
<atom:BreadcrumbItem>Application</atom:BreadcrumbItem>
|
||||
</atom:Breadcrumb>
|
||||
</StackPanel>
|
||||
</gallery:ShowCaseItem>
|
||||
|
||||
<gallery:ShowCaseItem
|
||||
Title="With Params"
|
||||
Description="With route params.">
|
||||
<StackPanel>
|
||||
<atom:Breadcrumb NavigateRequest="HandleNavigateRequest">
|
||||
<atom:BreadcrumbItem>Users</atom:BreadcrumbItem>
|
||||
<atom:BreadcrumbItem NavigateContext="Param(1)">Param</atom:BreadcrumbItem>
|
||||
</atom:Breadcrumb>
|
||||
</StackPanel>
|
||||
</gallery:ShowCaseItem>
|
||||
|
||||
<gallery:ShowCaseItem
|
||||
Title="Configuring the Separator"
|
||||
Description="The separator can be customized by setting the separator property: separator equals >">
|
||||
<StackPanel>
|
||||
<atom:Breadcrumb Separator=">">
|
||||
<atom:BreadcrumbItem>Home</atom:BreadcrumbItem>
|
||||
<atom:BreadcrumbItem NavigateContext="#">Application Center</atom:BreadcrumbItem>
|
||||
<atom:BreadcrumbItem NavigateContext="#">Application List</atom:BreadcrumbItem>
|
||||
<atom:BreadcrumbItem>An Application</atom:BreadcrumbItem>
|
||||
</atom:Breadcrumb>
|
||||
</StackPanel>
|
||||
</gallery:ShowCaseItem>
|
||||
|
||||
<gallery:ShowCaseItem
|
||||
Title="Configuring the Separator Independently"
|
||||
Description="Customize separator for each other.">
|
||||
<StackPanel>
|
||||
<atom:Breadcrumb>
|
||||
<atom:BreadcrumbItem Separator=":">Location</atom:BreadcrumbItem>
|
||||
<atom:BreadcrumbItem NavigateContext="#">Application Center</atom:BreadcrumbItem>
|
||||
<atom:BreadcrumbItem NavigateContext="#">Application List</atom:BreadcrumbItem>
|
||||
<atom:BreadcrumbItem>An Application</atom:BreadcrumbItem>
|
||||
</atom:Breadcrumb>
|
||||
</StackPanel>
|
||||
</gallery:ShowCaseItem>
|
||||
|
||||
<gallery:ShowCaseItem
|
||||
Title="Generate BreadcrumbItem by template"
|
||||
Description="Generate BreadcrumbItem by template.">
|
||||
<StackPanel>
|
||||
<atom:Breadcrumb Name="TplBreadcrumb">
|
||||
<atom:Breadcrumb.ItemTemplate>
|
||||
<DataTemplate x:DataType="atom:BreadcrumbItemData">
|
||||
<TextBlock Text="{Binding Content}"/>
|
||||
</DataTemplate>
|
||||
</atom:Breadcrumb.ItemTemplate>
|
||||
</atom:Breadcrumb>
|
||||
</StackPanel>
|
||||
</gallery:ShowCaseItem>
|
||||
</gallery:ShowCasePanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Disposables.Fluent;
|
||||
using AtomUI.Desktop.Controls;
|
||||
using AtomUIGallery.ShowCases.ViewModels;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Avalonia;
|
||||
|
||||
namespace AtomUIGallery.ShowCases.Views;
|
||||
|
||||
public partial class BreadcrumbShowCase : ReactiveUserControl<BreadcrumbViewModel>
|
||||
{
|
||||
private WindowMessageManager? _messageManager;
|
||||
|
||||
public BreadcrumbShowCase()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.WhenActivated(disposables =>
|
||||
{
|
||||
if (DataContext is BreadcrumbViewModel viewModel)
|
||||
{
|
||||
viewModel.BreadcrumbItems = [
|
||||
new BreadcrumbItemData()
|
||||
{
|
||||
Separator = ":",
|
||||
Content = "Location"
|
||||
},
|
||||
new BreadcrumbItemData()
|
||||
{
|
||||
NavigateContext = "#",
|
||||
Content = "Application Center"
|
||||
},
|
||||
new BreadcrumbItemData()
|
||||
{
|
||||
NavigateContext = "#",
|
||||
Content = "Application List"
|
||||
},
|
||||
new BreadcrumbItemData()
|
||||
{
|
||||
Content = "An Application"
|
||||
}
|
||||
];
|
||||
|
||||
this.OneWayBind(viewModel, vm => vm.BreadcrumbItems, v => v.TplBreadcrumb.ItemsSource)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
Disposable.Create(() =>
|
||||
{
|
||||
viewModel.BreadcrumbItems = null;
|
||||
}).DisposeWith(disposables);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
_messageManager = new WindowMessageManager(topLevel)
|
||||
{
|
||||
MaxItems = 10
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
_messageManager?.Dispose();
|
||||
_messageManager = null;
|
||||
}
|
||||
|
||||
private void HandleNavigateRequest(object? sender, BreadcrumbNavigateEventArgs eventArgs)
|
||||
{
|
||||
_messageManager?.Show(new Message(
|
||||
$"Navigate context: {eventArgs.BreadcrumbItem.NavigateContext}"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ internal class en_US : LanguageProvider
|
||||
public const string Layout = "Layout";
|
||||
public const string Layout_Splitter = "Splitter";
|
||||
public const string Navigation = "Navigation";
|
||||
public const string Navigation_Breadcrumb = "Breadcrumb";
|
||||
public const string DataEntry = "Data Entry";
|
||||
public const string DataDisplay = "Data Display";
|
||||
public const string Feedback = "Feedback";
|
||||
|
||||
@@ -16,6 +16,7 @@ internal class zh_CN : LanguageProvider
|
||||
public const string Layout = "布局";
|
||||
public const string Layout_Splitter = "Splitter 分割面板";
|
||||
public const string Navigation = "导航";
|
||||
public const string Navigation_Breadcrumb = "Breadcrumb 面包屑";
|
||||
public const string DataEntry = "数据录入";
|
||||
public const string DataDisplay = "数据展示";
|
||||
public const string Feedback = "反馈";
|
||||
|
||||
@@ -59,6 +59,7 @@ public class CaseNavigationViewModel : ReactiveObject, IActivatableViewModel
|
||||
_showCaseViewModelFactories.Add(ButtonViewModel.ID, () => new ButtonViewModel(HostScreen));
|
||||
_showCaseViewModelFactories.Add(SplitButtonViewModel.ID, () => new SplitButtonViewModel(HostScreen));
|
||||
_showCaseViewModelFactories.Add(SplitterViewModel.ID, () => new SplitterViewModel(HostScreen));
|
||||
_showCaseViewModelFactories.Add(BreadcrumbViewModel.ID, () => new BreadcrumbViewModel(HostScreen));
|
||||
}
|
||||
|
||||
private void DoNavigateTo(EntityKey showCaseId)
|
||||
|
||||
@@ -30,7 +30,10 @@
|
||||
</atom:NavMenuNode>
|
||||
|
||||
<atom:NavMenuNode Header="{gallery:CaseNavigationLangResource Navigation}"
|
||||
Icon="{antdicons:AntDesignIconProvider Kind=MenuOutlined}" />
|
||||
Icon="{antdicons:AntDesignIconProvider Kind=MenuOutlined}">
|
||||
<atom:NavMenuNode Header="{gallery:CaseNavigationLangResource Navigation_Breadcrumb}"
|
||||
ItemKey="{x:Static viewmodels:BreadcrumbViewModel.ID}" />
|
||||
</atom:NavMenuNode>
|
||||
|
||||
<atom:NavMenuNode Header="{gallery:CaseNavigationLangResource DataEntry}"
|
||||
Icon="{antdicons:AntDesignIconProvider Kind=FormOutlined}" />
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<!-- <ResourceInclude Source="Alert/Themes/AlertTheme.axaml" /> -->
|
||||
<!-- <ResourceInclude Source="Avatar/Themes/AvatarThemes.axaml" /> -->
|
||||
<!-- <ResourceInclude Source="Badge/Themes/BadgeThemes.axaml" /> -->
|
||||
<!-- <ResourceInclude Source="Breadcrumb/Themes/BreadcrumbThemes.axaml" /> -->
|
||||
<ResourceInclude Source="Breadcrumb/Themes/BreadcrumbThemes.axaml" />
|
||||
<ResourceInclude Source="Buttons/Themes/ButtonThemes.axaml" />
|
||||
<!-- <ResourceInclude Source="ButtonSpinner/Themes/ButtonSpinnerThemes.axaml" /> -->
|
||||
<!-- <ResourceInclude Source="Cascader/Themes/CascaderThemes.axaml" /> -->
|
||||
@@ -37,7 +37,7 @@
|
||||
<!-- <ResourceInclude Source="ListBox/Themes/ListBoxThemes.axaml" /> -->
|
||||
<!-- <ResourceInclude Source="Mentions/Themes/MentionsThemes.axaml" /> -->
|
||||
<ResourceInclude Source="Menu/Themes/MenuThemes.axaml" />
|
||||
<!-- <ResourceInclude Source="Message/Themes/MessageThemes.axaml" /> -->
|
||||
<ResourceInclude Source="Message/Themes/MessageThemes.axaml" />
|
||||
<!-- <ResourceInclude Source="MessageBox/Themes/MessageBoxThemes.axaml" /> -->
|
||||
<ResourceInclude Source="NavMenu/Themes/NavMenuThemes.axaml" />
|
||||
<!-- <ResourceInclude Source="Notifications/Themes/NotificationsThemes.axaml" /> -->
|
||||
@@ -60,7 +60,7 @@
|
||||
<!-- <ResourceInclude Source="Slider/Themes/SliderThemes.axaml" /> -->
|
||||
<ResourceInclude Source="Space/Themes/SpaceThemes.axaml" />
|
||||
<!-- <ResourceInclude Source="Spin/Themes/SpinThemes.axaml" /> -->
|
||||
<!-- <ResourceInclude Source="Splitter/Themes/SplitterThemes.axaml" /> -->
|
||||
<ResourceInclude Source="Splitter/Themes/SplitterThemes.axaml" />
|
||||
<!-- <ResourceInclude Source="SplitView/Themes/SplitViewTheme.axaml" /> -->
|
||||
<!-- <ResourceInclude Source="Statistic/Themes/StatisticThemes.axaml" /> -->
|
||||
<!-- <ResourceInclude Source="Steps/Themes/StepsThemes.axaml" /> -->
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ namespace AtomUI.Theme
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.ListBoxToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.MarqueeLabelToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.MenuToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.MessageToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.NavMenuToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.OptionButtonToken));
|
||||
tokenTypes.Add(typeof(AtomUI.Desktop.Controls.PopupHostToken));
|
||||
|
||||
+21
@@ -285,6 +285,16 @@ namespace AtomUI.Desktop.Controls.DesignTokens
|
||||
TopLevelItemSelectedColor
|
||||
}
|
||||
|
||||
public enum MessageTokenKind
|
||||
{
|
||||
CardHeight,
|
||||
ContentBg,
|
||||
ContentPadding,
|
||||
MessageIconMargin,
|
||||
MessageIconSize,
|
||||
MessageTopMargin
|
||||
}
|
||||
|
||||
public enum NavMenuTokenKind
|
||||
{
|
||||
ActiveBarHeight,
|
||||
@@ -935,6 +945,17 @@ namespace AtomUI.Desktop.Controls.DesignTokens
|
||||
}
|
||||
}
|
||||
|
||||
public class MessageTokenResourceExtension : TokenResourceExtension<MessageTokenKind>
|
||||
{
|
||||
public MessageTokenResourceExtension()
|
||||
{
|
||||
}
|
||||
|
||||
public MessageTokenResourceExtension(MessageTokenKind kind) : base(kind)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class NavMenuTokenResourceExtension : TokenResourceExtension<NavMenuTokenKind>
|
||||
{
|
||||
public NavMenuTokenResourceExtension()
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
public interface IMessageManager
|
||||
{
|
||||
public void Show(IMessage message, string[]? classes = null);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
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 (!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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
using AtomUI.Controls;
|
||||
using AtomUI.Icons.AntDesign;
|
||||
using AtomUI.MotionScene;
|
||||
using AtomUI.Theme;
|
||||
using Avalonia;
|
||||
using Avalonia.Animation.Easings;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[PseudoClasses(MessageCardPseudoClass.Error,
|
||||
MessageCardPseudoClass.Information,
|
||||
MessageCardPseudoClass.Success,
|
||||
MessageCardPseudoClass.Warning,
|
||||
MessageCardPseudoClass.Loading)]
|
||||
public class MessageCard : TemplatedControl, IMotionAwareControl
|
||||
{
|
||||
internal const double AnimationMaxOffsetY = 100d;
|
||||
|
||||
#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(MessageType));
|
||||
|
||||
/// <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<MessageCard, string>(nameof(Message));
|
||||
|
||||
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
|
||||
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<MessageCard>();
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
public bool IsMotionEnabled
|
||||
{
|
||||
get => GetValue(IsMotionEnabledProperty);
|
||||
set => SetValue(IsMotionEnabledProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部属性定义
|
||||
|
||||
/// <summary>回调:消息卡片关闭时触发,由 WindowMessageManager 设置以避免 lambda 闭包。</summary>
|
||||
internal Action? OnClose { get; set; }
|
||||
|
||||
internal static readonly DirectProperty<MessageCard, TimeSpan> OpenCloseMotionDurationProperty =
|
||||
AvaloniaProperty.RegisterDirect<MessageCard, TimeSpan>(nameof(OpenCloseMotionDuration),
|
||||
o => o.OpenCloseMotionDuration,
|
||||
(o, v) => o.OpenCloseMotionDuration = v);
|
||||
|
||||
private TimeSpan _openCloseMotionDuration;
|
||||
|
||||
internal TimeSpan OpenCloseMotionDuration
|
||||
{
|
||||
get => _openCloseMotionDuration;
|
||||
set => SetAndRaise(OpenCloseMotionDurationProperty, ref _openCloseMotionDuration, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool _isClosing;
|
||||
private BaseMotionActor? _motionActor;
|
||||
|
||||
public MessageCard()
|
||||
{
|
||||
this.RegisterTokenResourceScope(MessageToken.ScopeProvider);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (IsClosing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsClosing = true;
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
if (this.IsAttachedToVisualTree())
|
||||
{
|
||||
if (change.Property == MessageTypeProperty)
|
||||
{
|
||||
SetupDefaultMessageIcon();
|
||||
UpdatePseudoClasses();
|
||||
}
|
||||
}
|
||||
|
||||
if (change.Property == IconProperty)
|
||||
{
|
||||
if (Icon is null)
|
||||
{
|
||||
SetupDefaultMessageIcon();
|
||||
}
|
||||
}
|
||||
|
||||
if (change.Property == IsClosedProperty)
|
||||
{
|
||||
if (!IsClosing && !IsClosed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RaiseEvent(new RoutedEventArgs(MessageClosedEvent));
|
||||
}
|
||||
else if (change.Property == IsClosingProperty)
|
||||
{
|
||||
if (IsClosing)
|
||||
{
|
||||
Dispatcher.InvokeAsync(ApplyHideMotionAsync);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_motionActor = e.NameScope.Find<BaseMotionActor>(BaseMotionActor.MotionActorPart);
|
||||
Dispatcher.InvokeAsync(ApplyShowMotionAsync);
|
||||
UpdatePseudoClasses();
|
||||
SetupDefaultMessageIcon();
|
||||
}
|
||||
|
||||
private async Task ApplyShowMotionAsync()
|
||||
{
|
||||
if (_motionActor is not null)
|
||||
{
|
||||
if (IsMotionEnabled)
|
||||
{
|
||||
_motionActor.IsVisible = false;
|
||||
var motion = new MoveUpInMotion(AnimationMaxOffsetY, _openCloseMotionDuration, new CubicEaseOut());
|
||||
await motion.RunAsync(_motionActor, () => { _motionActor.IsVisible = true; });
|
||||
}
|
||||
else
|
||||
{
|
||||
_motionActor.IsVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyHideMotionAsync()
|
||||
{
|
||||
if (_motionActor is not null)
|
||||
{
|
||||
if (IsMotionEnabled)
|
||||
{
|
||||
var motion =
|
||||
new MoveUpOutMotion(AnimationMaxOffsetY, _openCloseMotionDuration, new CubicEaseIn());
|
||||
await motion.RunAsync(_motionActor);
|
||||
IsClosed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsClosed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePseudoClasses()
|
||||
{
|
||||
switch (MessageType)
|
||||
{
|
||||
case MessageType.Error:
|
||||
PseudoClasses.Add(MessageCardPseudoClass.Error);
|
||||
break;
|
||||
|
||||
case MessageType.Information:
|
||||
PseudoClasses.Add(MessageCardPseudoClass.Information);
|
||||
break;
|
||||
|
||||
case MessageType.Success:
|
||||
PseudoClasses.Add(MessageCardPseudoClass.Success);
|
||||
break;
|
||||
|
||||
case MessageType.Warning:
|
||||
PseudoClasses.Add(MessageCardPseudoClass.Warning);
|
||||
break;
|
||||
|
||||
case MessageType.Loading:
|
||||
PseudoClasses.Add(MessageCardPseudoClass.Loading);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupDefaultMessageIcon()
|
||||
{
|
||||
Icon? icon = null;
|
||||
if (MessageType == MessageType.Information)
|
||||
{
|
||||
icon = new InfoCircleFilled();
|
||||
}
|
||||
else if (MessageType == MessageType.Success)
|
||||
{
|
||||
icon = new CheckCircleFilled();
|
||||
}
|
||||
else if (MessageType == MessageType.Error)
|
||||
{
|
||||
icon = new CloseCircleFilled();
|
||||
}
|
||||
else if (MessageType == MessageType.Warning)
|
||||
{
|
||||
icon = new ExclamationCircleFilled();
|
||||
}
|
||||
else if (MessageType == MessageType.Loading)
|
||||
{
|
||||
icon = new LoadingOutlined();
|
||||
icon.LoadingAnimation = IconAnimation.Spin;
|
||||
}
|
||||
|
||||
if (Icon is null)
|
||||
{
|
||||
ClearValue(IconProperty);
|
||||
SetValue(IconProperty, icon, BindingPriority.Template);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
public static class MessageCardPseudoClass
|
||||
{
|
||||
public const string Error = ":error";
|
||||
public const string Information = ":information";
|
||||
public const string Success = ":success";
|
||||
public const string Warning = ":warning";
|
||||
public const string Loading = ":loading";
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using AtomUI.Desktop.Controls.DesignTokens;
|
||||
using AtomUI.Theme;
|
||||
using AtomUI.Theme.TokenSystem;
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[ControlDesignToken]
|
||||
internal class MessageToken : AbstractControlDesignToken
|
||||
{
|
||||
public const string ID = "Message";
|
||||
public static readonly ControlTokenResourceScopeProvider ScopeProvider = new(ID);
|
||||
|
||||
/// <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)
|
||||
{
|
||||
}
|
||||
|
||||
public override void CalculateTokenValues(bool isDarkMode)
|
||||
{
|
||||
base.CalculateTokenValues(isDarkMode);
|
||||
ContentBg = SharedToken.ColorBgElevated;
|
||||
ContentPadding = new Thickness(
|
||||
(SharedToken.ControlHeightLG -
|
||||
SharedToken.FontSize * SharedToken.RelativeLineHeight) / 2,
|
||||
SharedToken.UniformlyPaddingXS);
|
||||
MessageIconMargin = new Thickness(0, 0, SharedToken.UniformlyMarginXS, 0);
|
||||
MessageTopMargin = new Thickness(SharedToken.UniformlyMargin, SharedToken.UniformlyMargin, SharedToken.UniformlyMargin, 0);
|
||||
MessageIconSize = SharedToken.FontSizeSM * SharedToken.RelativeLineHeightSM;
|
||||
}
|
||||
|
||||
protected override Type GetTokenKindType() => typeof(MessageTokenKind);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
public enum MessageType
|
||||
{
|
||||
Information,
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
Loading
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:ClassModifier="internal">
|
||||
<ControlTheme x:Key="{x:Type atom:MessageCard}" TargetType="atom:MessageCard">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<atom:MotionActor Name="{x:Static atom:BaseMotionActor.MotionActorPart}"
|
||||
ClipToBounds="False">
|
||||
<Border Name="{x:Static atom:MessageCardThemeConstants.FramePart}">
|
||||
<DockPanel Name="{x:Static atom:MessageCardThemeConstants.HeaderContainerPart}"
|
||||
LastChildFill="True">
|
||||
<atom:IconPresenter Name="{x:Static atom:MessageCardThemeConstants.IconContentPart}"
|
||||
DockPanel.Dock="Left"
|
||||
IsVisible="{TemplateBinding Icon, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||
Icon="{TemplateBinding Icon}" />
|
||||
<SelectableTextBlock Name="{x:Static atom:MessageCardThemeConstants.MessagePart}"
|
||||
Text="{TemplateBinding Message}" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</atom:MotionActor>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Setter Property="IsMotionEnabled" Value="{atom:SharedTokenResource EnableMotion}" />
|
||||
|
||||
<Style Selector="^ /template/ atom|IconPresenter">
|
||||
<Setter Property="Width" Value="{atom:MessageTokenResource MessageIconSize}" />
|
||||
<Setter Property="Height" Value="{atom:MessageTokenResource MessageIconSize}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[MessageType=Information]">
|
||||
<Style Selector="^ /template/ atom|IconPresenter">
|
||||
<Setter Property="IconBrush"
|
||||
Value="{atom:SharedTokenResource ColorPrimary}" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[MessageType=Loading]">
|
||||
<Style Selector="^ /template/ atom|IconPresenter">
|
||||
<Setter Property="IconBrush"
|
||||
Value="{atom:SharedTokenResource ColorPrimary}" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[MessageType=Error]">
|
||||
<Style Selector="^ /template/ atom|IconPresenter">
|
||||
<Setter Property="IconBrush"
|
||||
Value="{atom:SharedTokenResource ColorError}" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[MessageType=Success]">
|
||||
<Style Selector="^ /template/ atom|IconPresenter">
|
||||
<Setter Property="IconBrush"
|
||||
Value="{atom:SharedTokenResource ColorSuccess}" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[MessageType=Warning]">
|
||||
<Style Selector="^ /template/ atom|IconPresenter">
|
||||
<Setter Property="IconBrush"
|
||||
Value="{atom:SharedTokenResource ColorWarning}" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Setter Property="OpenCloseMotionDuration"
|
||||
Value="{atom:SharedTokenResource MotionDurationMid}" />
|
||||
<Setter Property="ClipToBounds" Value="False" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
|
||||
<Style Selector="^ /template/ Border#PART_Frame">
|
||||
<Setter Property="Margin" Value="{atom:MessageTokenResource MessageTopMargin}" />
|
||||
<Setter Property="Padding" Value="{atom:MessageTokenResource ContentPadding}" />
|
||||
<Setter Property="BoxShadow" Value="{atom:SharedTokenResource BoxShadows}" />
|
||||
<Setter Property="Background" Value="{atom:MessageTokenResource ContentBg}" />
|
||||
<Setter Property="CornerRadius" Value="{atom:SharedTokenResource BorderRadiusLG}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^[IsClosed=True]">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^ /template/ SelectableTextBlock#PART_Message">
|
||||
<Setter Property="LineHeight" Value="{atom:SharedTokenResource FontHeight}" />
|
||||
<Setter Property="FontSize" Value="{atom:SharedTokenResource FontSize}" />
|
||||
<Setter Property="Foreground" Value="{atom:SharedTokenResource ColorText}" />
|
||||
<Setter Property="SelectionBrush" Value="{atom:SharedTokenResource SelectionBackground}" />
|
||||
<Setter Property="SelectionForegroundBrush" Value="{atom:SharedTokenResource SelectionForeground}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="^ /template/ atom|IconPresenter#PART_IconContent">
|
||||
<Setter Property="Margin" Value="{atom:MessageTokenResource MessageIconMargin}" />
|
||||
</Style>
|
||||
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace AtomUI.Desktop.Controls.Themes;
|
||||
|
||||
internal class WindowMessageManagerThemeConstants
|
||||
{
|
||||
public const string ItemsPart = "PART_Items";
|
||||
}
|
||||
|
||||
internal class MessageCardThemeConstants
|
||||
{
|
||||
public const string FramePart = "PART_Frame";
|
||||
public const string IconContentPart = "PART_IconContent";
|
||||
public const string HeaderContainerPart = "PART_HeaderContainer";
|
||||
public const string MessagePart = "PART_Message";
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:ClassModifier="internal">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="MessageCardTheme.axaml"/>
|
||||
<ResourceInclude Source="WindowMessageManagerTheme.axaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,23 @@
|
||||
<ResourceDictionary
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:atom="https://atomui.net"
|
||||
x:ClassModifier="internal">
|
||||
<ControlTheme x:Key="{x:Type atom:WindowMessageManager}" TargetType="atom:WindowMessageManager">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<ReversibleStackPanel Name="{x:Static atom:WindowMessageManagerThemeConstants.ItemsPart}" />
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
|
||||
<Setter Property="IsMotionEnabled" Value="{atom:SharedTokenResource EnableMotion}" />
|
||||
|
||||
<Style Selector="^:topcenter">
|
||||
<Style Selector="^ /template/ ReversibleStackPanel#PART_Items">
|
||||
<Setter Property="ReverseOrder" Value="True"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalAlignment" Value="Top"/>
|
||||
</Style>
|
||||
</Style>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,235 @@
|
||||
using System.Collections;
|
||||
using AtomUI.Controls;
|
||||
using AtomUI.Theme;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.Controls.Notifications;
|
||||
|
||||
namespace AtomUI.Desktop.Controls;
|
||||
|
||||
[TemplatePart("PART_Items", typeof(Panel))]
|
||||
public class WindowMessageManager : TemplatedControl, IMessageManager, IMotionAwareControl, IDisposable
|
||||
{
|
||||
#region 公共属性定义
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Position" /> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<NotificationPosition> PositionProperty =
|
||||
AvaloniaProperty.Register<WindowMessageManager, NotificationPosition>(
|
||||
nameof(Position), NotificationPosition.TopRight);
|
||||
|
||||
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
|
||||
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<WindowMessageManager>();
|
||||
|
||||
/// <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<WindowMessageManager, 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);
|
||||
}
|
||||
|
||||
public bool IsMotionEnabled
|
||||
{
|
||||
get => GetValue(IsMotionEnabledProperty);
|
||||
set => SetValue(IsMotionEnabledProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private IList? _items;
|
||||
private TopLevel? _topLevel;
|
||||
private bool _isDisposed;
|
||||
private AdornerLayer? _adornerLayer;
|
||||
|
||||
/// <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)
|
||||
{
|
||||
this.RegisterTokenResourceScope(MessageToken.ScopeProvider);
|
||||
|
||||
if (host is not null)
|
||||
{
|
||||
_topLevel = host;
|
||||
InstallFromTopLevel(host);
|
||||
}
|
||||
}
|
||||
|
||||
static WindowMessageManager()
|
||||
{
|
||||
HorizontalAlignmentProperty.OverrideDefaultValue<WindowMessageManager>(HorizontalAlignment.Stretch);
|
||||
VerticalAlignmentProperty.OverrideDefaultValue<WindowMessageManager>(VerticalAlignment.Stretch);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
|
||||
var itemsControl = e.NameScope.Find<Panel>("PART_Items");
|
||||
_items = itemsControl?.Children;
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
_items?.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows a Notification
|
||||
/// </summary>
|
||||
/// <param name="message">the content of the message</param>
|
||||
/// <param name="classes">style classes to apply</param>
|
||||
public void Show(IMessage message, string[]? classes = null)
|
||||
{
|
||||
if (_isDisposed || _items is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.VerifyAccess();
|
||||
|
||||
var messageControl = new MessageCard
|
||||
{
|
||||
Icon = message.Icon,
|
||||
Message = message.Content,
|
||||
MessageType = message.Type
|
||||
};
|
||||
messageControl[!MessageCard.IsMotionEnabledProperty] = this[!IsMotionEnabledProperty];
|
||||
|
||||
// Add style classes if any
|
||||
if (classes?.Length > 0)
|
||||
{
|
||||
foreach (var cls in classes)
|
||||
{
|
||||
messageControl.Classes.Add(cls);
|
||||
}
|
||||
}
|
||||
|
||||
messageControl.OnClose = message.OnClose;
|
||||
messageControl.MessageClosed += OnMessageClosed;
|
||||
|
||||
Dispatcher.Post(() =>
|
||||
{
|
||||
_items.Add(messageControl);
|
||||
RemoveExcessMessages();
|
||||
});
|
||||
|
||||
// Auto-close after expiration time
|
||||
if (message.Expiration != TimeSpan.Zero)
|
||||
{
|
||||
DispatcherTimer.RunOnce(messageControl.Close, message.Expiration);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMessageClosed(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is MessageCard card)
|
||||
{
|
||||
card.OnClose?.Invoke();
|
||||
_items?.Remove(card);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes excess messages when the count exceeds MaxItems.
|
||||
/// </summary>
|
||||
private void RemoveExcessMessages()
|
||||
{
|
||||
var visibleMessages = _items!.OfType<MessageCard>().Where(m => !m.IsClosing).ToList();
|
||||
var excessCount = visibleMessages.Count - MaxItems;
|
||||
|
||||
if (excessCount > 0)
|
||||
{
|
||||
for (int i = 0; i < excessCount; i++)
|
||||
{
|
||||
visibleMessages[i].Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InstallFromTopLevel(TopLevel topLevel)
|
||||
{
|
||||
topLevel.TemplateApplied += TopLevelOnTemplateApplied;
|
||||
_adornerLayer = AdornerLayer.GetAdornerLayer(topLevel);
|
||||
if (_adornerLayer is not null)
|
||||
{
|
||||
_adornerLayer.Children.Add(this);
|
||||
AdornerLayer.SetAdornedElement(this, _adornerLayer);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_topLevel is null || _isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 卸载事件订阅
|
||||
_topLevel.TemplateApplied -= TopLevelOnTemplateApplied;
|
||||
_items?.Clear();
|
||||
// 从 AdornerLayer 中移除
|
||||
if (_adornerLayer is not null)
|
||||
{
|
||||
_adornerLayer.Children.Remove(this);
|
||||
AdornerLayer.SetAdornedElement(this, null);
|
||||
_adornerLayer = null;
|
||||
}
|
||||
_topLevel = null;
|
||||
_items = null;
|
||||
_isDisposed = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Error uninstalling from TopLevel: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveFromAdornerLayer()
|
||||
{
|
||||
if (_adornerLayer is not null)
|
||||
{
|
||||
_adornerLayer.Children.Remove(this);
|
||||
AdornerLayer.SetAdornedElement(this, null);
|
||||
_adornerLayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void TopLevelOnTemplateApplied(object? sender, TemplateAppliedEventArgs _)
|
||||
{
|
||||
RemoveFromAdornerLayer();
|
||||
|
||||
// Reinstall notification manager on template reapplied.
|
||||
var topLevel = (TopLevel)sender!;
|
||||
topLevel.TemplateApplied -= TopLevelOnTemplateApplied;
|
||||
InstallFromTopLevel(topLevel);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user