221 lines
7.8 KiB
C#
221 lines
7.8 KiB
C#
|
|
using System;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Linq;
|
|||
|
|
using System.Runtime.CompilerServices;
|
|||
|
|
using System.Text;
|
|||
|
|
using System.Threading.Tasks;
|
|||
|
|
using System.Windows.Documents;
|
|||
|
|
using System.Windows.Media.Animation;
|
|||
|
|
using System.Windows.Threading;
|
|||
|
|
|
|||
|
|
|
|||
|
|
namespace NeuWPF.Controls;
|
|||
|
|
|
|||
|
|
public static class Toast
|
|||
|
|
{
|
|||
|
|
public static IToastService Screen { get; }
|
|||
|
|
private static readonly ConditionalWeakTable<Window, IToastService> WindowManagers = new();
|
|||
|
|
static Toast() { Screen = new ToastManager(new ScreenHost()); }
|
|||
|
|
public static IToastService For(Window window)
|
|||
|
|
{
|
|||
|
|
if (window == null) throw new ArgumentNullException(nameof(window));
|
|||
|
|
return WindowManagers.GetValue(window, w => new ToastManager(new WindowHost(w)));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public interface IToastService
|
|||
|
|
{
|
|||
|
|
void Show(string message, ToastType type = ToastType.Info, TimeSpan? duration = null);
|
|||
|
|
void ShowSuccess(string message, TimeSpan? duration = null);
|
|||
|
|
void ShowError(string message, TimeSpan? duration = null);
|
|||
|
|
void ShowWarning(string message, TimeSpan? duration = null);
|
|||
|
|
void ShowInfo(string message, TimeSpan? duration = null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#region Internal Implementation
|
|||
|
|
|
|||
|
|
internal class ToastManager : IToastService
|
|||
|
|
{
|
|||
|
|
private readonly IToastHost _host;
|
|||
|
|
public ToastManager(IToastHost host) => _host = host;
|
|||
|
|
|
|||
|
|
public void Show(string message, ToastType type, TimeSpan? duration = null)
|
|||
|
|
{
|
|||
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|||
|
|
{
|
|||
|
|
var toast = new ToastControl { Message = message, Type = type };
|
|||
|
|
|
|||
|
|
var fadeInAnim = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(350));
|
|||
|
|
|
|||
|
|
var timer = new DispatcherTimer { Interval = duration ?? TimeSpan.FromSeconds(4) };
|
|||
|
|
timer.Tick += (sender, args) =>
|
|||
|
|
{
|
|||
|
|
timer.Stop();
|
|||
|
|
var fadeOutAnim = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(350));
|
|||
|
|
fadeOutAnim.Completed += (s, e) => _host.RemoveToast(toast);
|
|||
|
|
toast.BeginAnimation(UIElement.OpacityProperty, fadeOutAnim);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
toast.Loaded += (sender, args) =>
|
|||
|
|
{
|
|||
|
|
toast.BeginAnimation(UIElement.OpacityProperty, fadeInAnim);
|
|||
|
|
timer.Start();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
_host.AddToast(toast);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void ShowSuccess(string message, TimeSpan? duration = null) => Show(message, ToastType.Success, duration);
|
|||
|
|
public void ShowError(string message, TimeSpan? duration = null) => Show(message, ToastType.Error, duration);
|
|||
|
|
public void ShowWarning(string message, TimeSpan? duration = null) => Show(message, ToastType.Warning, duration);
|
|||
|
|
public void ShowInfo(string message, TimeSpan? duration = null) => Show(message, ToastType.Info, duration);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal interface IToastHost
|
|||
|
|
{
|
|||
|
|
void AddToast(ToastControl toast);
|
|||
|
|
void RemoveToast(ToastControl toast);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
internal class WindowHost : IToastHost
|
|||
|
|
{
|
|||
|
|
private readonly StackPanel _toastContainer;
|
|||
|
|
|
|||
|
|
public WindowHost(Window window)
|
|||
|
|
{
|
|||
|
|
_toastContainer = new StackPanel();
|
|||
|
|
|
|||
|
|
// Use a style to apply margin between toasts, which is a robust approach.
|
|||
|
|
var childStyle = new Style(typeof(ToastControl));
|
|||
|
|
childStyle.Setters.Add(new Setter(FrameworkElement.MarginProperty, new Thickness(0, 8, 0, 8)));
|
|||
|
|
_toastContainer.Resources.Add(typeof(ToastControl), childStyle);
|
|||
|
|
|
|||
|
|
var adornerLayer = AdornerLayer.GetAdornerLayer(window.Content as Visual);
|
|||
|
|
if (adornerLayer == null) throw new InvalidOperationException("Cannot find AdornerLayer on the specified window.");
|
|||
|
|
|
|||
|
|
adornerLayer.Add(new ToastAdorner(window.Content as UIElement, _toastContainer));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void AddToast(ToastControl toast) => _toastContainer.Children.Add(toast);
|
|||
|
|
public void RemoveToast(ToastControl toast) => _toastContainer.Children.Remove(toast);
|
|||
|
|
|
|||
|
|
|
|||
|
|
private class ToastAdorner : Adorner
|
|||
|
|
{
|
|||
|
|
private readonly UIElement _child; // This is our StackPanel
|
|||
|
|
|
|||
|
|
public ToastAdorner(UIElement adornedElement, UIElement child) : base(adornedElement)
|
|||
|
|
{
|
|||
|
|
_child = child;
|
|||
|
|
// Add the child to the visual tree. This is essential.
|
|||
|
|
AddVisualChild(_child);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected override int VisualChildrenCount => 1;
|
|||
|
|
protected override Visual GetVisualChild(int index) => _child;
|
|||
|
|
|
|||
|
|
// We override Measure to tell our child (StackPanel) how much space it has.
|
|||
|
|
protected override Size MeasureOverride(Size constraint)
|
|||
|
|
{
|
|||
|
|
// Give the child all the space it could possibly want.
|
|||
|
|
// It will then measure itself and its children, establishing its DesiredSize.
|
|||
|
|
_child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
|||
|
|
// The Adorner itself doesn't need to return a specific size, as it's a transparent overlay.
|
|||
|
|
return base.MeasureOverride(constraint);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ArrangeOverride is where we do the actual positioning.
|
|||
|
|
protected override Size ArrangeOverride(Size finalSize)
|
|||
|
|
{
|
|||
|
|
// 'finalSize' is the full size of the element we are adorning (e.g., the window's content area).
|
|||
|
|
// '_child.DesiredSize' is the size our StackPanel reported it needs after being measured.
|
|||
|
|
|
|||
|
|
// Calculate the X coordinate to center the child (StackPanel).
|
|||
|
|
double x = (finalSize.Width - _child.DesiredSize.Width) / 2;
|
|||
|
|
|
|||
|
|
// The Y coordinate is 0, placing it at the very top.
|
|||
|
|
double y = 0;
|
|||
|
|
|
|||
|
|
// Create the final rectangle for our child, positioned correctly.
|
|||
|
|
Rect finalRect = new Rect(new Point(x, y), _child.DesiredSize);
|
|||
|
|
|
|||
|
|
// Arrange the child (our StackPanel) in the calculated centered rectangle.
|
|||
|
|
_child.Arrange(finalRect);
|
|||
|
|
|
|||
|
|
// The Adorner itself arranges to fill the entire space.
|
|||
|
|
return finalSize;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
internal class ScreenHost : IToastHost
|
|||
|
|
{
|
|||
|
|
private readonly List<Window> _toastWindows = new();
|
|||
|
|
private readonly object _lock = new();
|
|||
|
|
private const double Spacing = 15;
|
|||
|
|
|
|||
|
|
public void AddToast(ToastControl toast)
|
|||
|
|
{
|
|||
|
|
var window = new Window
|
|||
|
|
{
|
|||
|
|
Content = toast,
|
|||
|
|
WindowStyle = WindowStyle.None,
|
|||
|
|
AllowsTransparency = true,
|
|||
|
|
Background = Brushes.Transparent,
|
|||
|
|
ShowInTaskbar = false,
|
|||
|
|
Topmost = true,
|
|||
|
|
Owner = null,
|
|||
|
|
SizeToContent = SizeToContent.WidthAndHeight,
|
|||
|
|
Left = -9999
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
window.Loaded += (sender, args) =>
|
|||
|
|
{
|
|||
|
|
lock (_lock)
|
|||
|
|
{
|
|||
|
|
_toastWindows.Add(window);
|
|||
|
|
RepositionToasts();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
window.Closed += (sender, args) =>
|
|||
|
|
{
|
|||
|
|
lock (_lock)
|
|||
|
|
{
|
|||
|
|
_toastWindows.Remove(window);
|
|||
|
|
RepositionToasts();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
window.Show();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void RemoveToast(ToastControl toast)
|
|||
|
|
{
|
|||
|
|
var window = _toastWindows.FirstOrDefault(w => w.Content == toast);
|
|||
|
|
window?.Close();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void RepositionToasts()
|
|||
|
|
{
|
|||
|
|
var workArea = SystemParameters.WorkArea;
|
|||
|
|
double currentTop = Spacing; // Start from the top with spacing.
|
|||
|
|
|
|||
|
|
foreach (var window in _toastWindows)
|
|||
|
|
{
|
|||
|
|
// Horizontally center the window on the screen.
|
|||
|
|
double left = workArea.Left + (workArea.Width - window.ActualWidth) / 2;
|
|||
|
|
|
|||
|
|
var moveAnim = new DoubleAnimation(currentTop, TimeSpan.FromMilliseconds(350))
|
|||
|
|
{ EasingFunction = new QuinticEase { EasingMode = EasingMode.EaseOut } };
|
|||
|
|
|
|||
|
|
window.Left = left; // Set horizontal position directly.
|
|||
|
|
window.BeginAnimation(Window.TopProperty, moveAnim); // Animate vertical position.
|
|||
|
|
|
|||
|
|
// Update the top position for the next toast.
|
|||
|
|
currentTop += window.ActualHeight + Spacing;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
#endregion
|