diff --git a/demo/Sandbox/Views/MainWindow.axaml b/demo/Sandbox/Views/MainWindow.axaml index d1f30fd..a4db4ff 100644 --- a/demo/Sandbox/Views/MainWindow.axaml +++ b/demo/Sandbox/Views/MainWindow.axaml @@ -7,27 +7,19 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Sandbox.Views.MainWindow" x:DataType="vm:MainWindowViewModel" + xmlns:sys="using:System" Icon="/Assets/avalonia-logo.ico" Title="Sandbox"> - + - - - - - - - - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/demo/Sandbox/Views/MainWindow.axaml.cs b/demo/Sandbox/Views/MainWindow.axaml.cs index 7430fad..937a796 100644 --- a/demo/Sandbox/Views/MainWindow.axaml.cs +++ b/demo/Sandbox/Views/MainWindow.axaml.cs @@ -1,4 +1,6 @@ using Avalonia.Controls; +using Avalonia.Interactivity; +using Ursa.Controls; namespace Sandbox.Views; @@ -8,4 +10,9 @@ public partial class MainWindow : Window { InitializeComponent(); } + + private async void Button_OnClick(object? sender, RoutedEventArgs e) + { + var res = await OverlayDialog.ShowModal(new TextBlock() { Text = "sdfksjdl" }, "root"); + } } \ No newline at end of file diff --git a/demo/Sandbox/Views/PW.axaml b/demo/Sandbox/Views/PW.axaml new file mode 100644 index 0000000..48b31a9 --- /dev/null +++ b/demo/Sandbox/Views/PW.axaml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/demo/Sandbox/Views/PW.axaml.cs b/demo/Sandbox/Views/PW.axaml.cs new file mode 100644 index 0000000..0cc6d85 --- /dev/null +++ b/demo/Sandbox/Views/PW.axaml.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Irihi.Avalonia.Shared.Contracts; +using Sandbox.ViewModels; +using Ursa.Controls; + +namespace Sandbox.Views; + +public partial class PW : UserControl +{ + public PW() + { + InitializeComponent(); + _overlayDialogHost.HostId = _hostid; + } + + private string _hostid = Path.GetRandomFileName(); + + private async void Button_OnClick(object? sender, RoutedEventArgs e) + { + Drawer.ShowCustom(new PW(), new TestVM(), _hostid); + } + + private void Close(object? sender, RoutedEventArgs e) + { + (DataContext as TestVM)?.Close(); + } +} + +public class TestVM : ViewModelBase, IDialogContext +{ + public void Close() + { + RequestClose?.Invoke(this, 12456789); + } + + public event EventHandler? RequestClose; +} \ No newline at end of file diff --git a/src/Ursa/Controls/Drawer/DrawerControlBase.cs b/src/Ursa/Controls/Drawer/DrawerControlBase.cs index 3da1f0e..326ff3b 100644 --- a/src/Ursa/Controls/Drawer/DrawerControlBase.cs +++ b/src/Ursa/Controls/Drawer/DrawerControlBase.cs @@ -49,11 +49,6 @@ public abstract class DrawerControlBase: OverlayFeedbackElement internal bool? IsCloseButtonVisible { get; set; } protected internal bool CanLightDismiss { get; set; } - - static DrawerControlBase() - { - DataContextProperty.Changed.AddClassHandler((o, e) => o.OnDataContextChange(e)); - } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -63,23 +58,6 @@ public abstract class DrawerControlBase: OverlayFeedbackElement Button.ClickEvent.AddHandler(OnCloseButtonClick, _closeButton); } - private void OnDataContextChange(AvaloniaPropertyChangedEventArgs args) - { - if(args.OldValue.Value is IDialogContext oldContext) - { - oldContext.RequestClose -= OnContextRequestClose; - } - if(args.NewValue.Value is IDialogContext newContext) - { - newContext.RequestClose += OnContextRequestClose; - } - } - - private void OnContextRequestClose(object? sender, object? e) - { - RaiseEvent(new ResultEventArgs(ClosedEvent, e)); - } - private void OnCloseButtonClick(object? sender, RoutedEventArgs e) => Close(); public override void Close() diff --git a/src/Ursa/Controls/OverlayShared/OverlayDialogHost.Dialog.cs b/src/Ursa/Controls/OverlayShared/OverlayDialogHost.Dialog.cs index 5a8a4f2..caa9691 100644 --- a/src/Ursa/Controls/OverlayShared/OverlayDialogHost.Dialog.cs +++ b/src/Ursa/Controls/OverlayShared/OverlayDialogHost.Dialog.cs @@ -1,6 +1,8 @@ using Avalonia; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.VisualTree; +using Irihi.Avalonia.Shared.Contracts; using Irihi.Avalonia.Shared.Helpers; using Irihi.Avalonia.Shared.Shapes; using Ursa.Controls.OverlayShared; @@ -63,6 +65,8 @@ public partial class OverlayDialogHost private async void OnDialogControlClosing(object? sender, object? e) { if (sender is not DialogControlBase control) return; + if (control.IsShowAsync is false && e is RoutedEventArgs args) + args.Handled = true; var layer = _layers.FirstOrDefault(a => a.Element == control); if (layer is null) return; _layers.Remove(layer); @@ -116,11 +120,12 @@ public partial class OverlayDialogHost if (!IsAnimationDisabled) MaskAppearAnimation.RunAsync(mask); var element = control.GetVisualDescendants().OfType() - .FirstOrDefault(FocusHelper.GetDialogFocusHint); + .FirstOrDefault(FocusHelper.GetDialogFocusHint); if (element is null) { element = control.GetVisualDescendants().OfType().FirstOrDefault(a => a.Focusable); } + element?.Focus(); _modalCount++; IsInModalStatus = _modalCount > 0; diff --git a/src/Ursa/Controls/OverlayShared/OverlayDialogHost.Drawer.cs b/src/Ursa/Controls/OverlayShared/OverlayDialogHost.Drawer.cs index 3c3a35b..17ec24a 100644 --- a/src/Ursa/Controls/OverlayShared/OverlayDialogHost.Drawer.cs +++ b/src/Ursa/Controls/OverlayShared/OverlayDialogHost.Drawer.cs @@ -5,6 +5,7 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Styling; using Avalonia.VisualTree; +using Irihi.Avalonia.Shared.Contracts; using Irihi.Avalonia.Shared.Shapes; using Ursa.Common; using Ursa.Controls.OverlayShared; @@ -22,9 +23,10 @@ public partial class OverlayDialogHost { mask = CreateOverlayMask(false, true); } + _layers.Add(new DialogPair(mask, control)); ResetZIndices(); - if(mask is not null)this.Children.Add(mask); + if (mask is not null) this.Children.Add(mask); this.Children.Add(control); control.Measure(this.Bounds.Size); control.Arrange(new Rect(control.DesiredSize)); @@ -47,7 +49,7 @@ public partial class OverlayDialogHost } } } - + internal async void AddModalDrawer(DrawerControlBase control) { PureRectangle mask = CreateOverlayMask(true, control.CanLightDismiss); @@ -72,26 +74,28 @@ public partial class OverlayDialogHost } var element = control.GetVisualDescendants().OfType() - .FirstOrDefault(FocusHelper.GetDialogFocusHint); + .FirstOrDefault(FocusHelper.GetDialogFocusHint); if (element is null) { element = control.GetVisualDescendants().OfType().FirstOrDefault(a => a.Focusable); } + element?.Focus(); } private void SetDrawerPosition(DrawerControlBase control) { - if(control.Position is Position.Left or Position.Right) + if (control.Position is Position.Left or Position.Right) { control.Height = this.Bounds.Height; } - if(control.Position is Position.Top or Position.Bottom) + + if (control.Position is Position.Top or Position.Bottom) { control.Width = this.Bounds.Width; } } - + private static void ResetDrawerPosition(DrawerControlBase control, Size newSize) { if (control.Position == Position.Right) @@ -112,7 +116,7 @@ public partial class OverlayDialogHost else { control.Width = newSize.Width; - SetTop(control, newSize.Height-control.Bounds.Height); + SetTop(control, newSize.Height - control.Bounds.Height); } } @@ -132,26 +136,28 @@ public partial class OverlayDialogHost source = appear ? Bounds.Width : Bounds.Width - elementBounds.Width; target = appear ? Bounds.Width - elementBounds.Width : Bounds.Width; } - + if (position == Position.Top) { source = appear ? -elementBounds.Height : 0; target = appear ? 0 : -elementBounds.Height; } - + if (position == Position.Bottom) { source = appear ? Bounds.Height : Bounds.Height - elementBounds.Height; target = appear ? Bounds.Height - elementBounds.Height : Bounds.Height; } - - var targetProperty = position==Position.Left || position==Position.Right ? Canvas.LeftProperty : Canvas.TopProperty; + + var targetProperty = position == Position.Left || position == Position.Right + ? Canvas.LeftProperty + : Canvas.TopProperty; var animation = new Animation { Easing = new CubicEaseOut(), FillMode = FillMode.Forward }; - var keyFrame1 = new KeyFrame(){ Cue = new Cue(0.0) }; + var keyFrame1 = new KeyFrame() { Cue = new Cue(0.0) }; keyFrame1.Setters.Add(new Setter() { Property = targetProperty, Value = source }); var keyFrame2 = new KeyFrame() { Cue = new Cue(1.0) }; @@ -162,16 +168,19 @@ public partial class OverlayDialogHost animation.Duration = TimeSpan.FromSeconds(0.3); return animation; } - + private async void OnDrawerControlClosing(object? sender, ResultEventArgs e) { if (sender is DrawerControlBase control) { + if (control.IsShowAsync is false) + e.Handled = true; + var layer = _layers.FirstOrDefault(a => a.Element == control); - if(layer is null) return; + if (layer is null) return; _layers.Remove(layer); control.RemoveHandler(OverlayFeedbackElement.ClosedEvent, OnDialogControlClosing); - control.RemoveHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged); + control.RemoveHandler(DialogControlBase.LayerChangedEvent, OnDialogLayerChanged); if (layer.Mask is not null) { _modalCount--; @@ -181,8 +190,10 @@ public partial class OverlayDialogHost if (!IsAnimationDisabled) { var disappearAnimation = CreateAnimation(control.Bounds.Size, control.Position, false); - await Task.WhenAll(disappearAnimation.RunAsync(control), MaskDisappearAnimation.RunAsync(layer.Mask)); + await Task.WhenAll(disappearAnimation.RunAsync(control), + MaskDisappearAnimation.RunAsync(layer.Mask)); } + Children.Remove(layer.Mask); } else @@ -193,6 +204,7 @@ public partial class OverlayDialogHost await disappearAnimation.RunAsync(control); } } + Children.Remove(control); ResetZIndices(); } diff --git a/src/Ursa/Controls/OverlayShared/OverlayFeedbackElement.cs b/src/Ursa/Controls/OverlayShared/OverlayFeedbackElement.cs index e437156..c60f615 100644 --- a/src/Ursa/Controls/OverlayShared/OverlayFeedbackElement.cs +++ b/src/Ursa/Controls/OverlayShared/OverlayFeedbackElement.cs @@ -2,6 +2,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.LogicalTree; using Avalonia.Threading; using Avalonia.VisualTree; using Irihi.Avalonia.Shared.Contracts; @@ -35,19 +36,13 @@ public abstract class OverlayFeedbackElement : ContentControl ClosedEvent.AddClassHandler((o, e) => o.OnClosed(e)); } - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - Content = null; - } - public bool IsClosed { get => GetValue(IsClosedProperty); set => SetValue(IsClosedProperty, value); } - private void OnClosed(ResultEventArgs _) + private void OnClosed(ResultEventArgs args) { SetCurrentValue(IsClosedProperty, true); } @@ -74,13 +69,25 @@ public abstract class OverlayFeedbackElement : ContentControl RaiseEvent(new ResultEventArgs(ClosedEvent, args)); } + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnDetachedFromLogicalTree(e); + Content = null; + } + + internal bool IsShowAsync { get; set; } + public Task ShowAsync(CancellationToken? token = default) { + IsShowAsync = true; var tcs = new TaskCompletionSource(); token?.Register(() => { Dispatcher.UIThread.Invoke(Close); }); void OnCloseHandler(object? sender, ResultEventArgs? args) { + IsShowAsync = false; + if (args is not null) + args.Handled = true; if (args?.Result is T result) tcs.SetResult(result); else diff --git a/tests/HeadlessTest.Ursa/Controls/DrawerTests/CloseEventTest/DrawerCloseEventTest.cs b/tests/HeadlessTest.Ursa/Controls/DrawerTests/CloseEventTest/DrawerCloseEventTest.cs new file mode 100644 index 0000000..9dddfbd --- /dev/null +++ b/tests/HeadlessTest.Ursa/Controls/DrawerTests/CloseEventTest/DrawerCloseEventTest.cs @@ -0,0 +1,63 @@ +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using Avalonia.Threading; +using Avalonia.VisualTree; +using Ursa.Controls; + +namespace HeadlessTest.Ursa.Controls; + +public class DrawerCloseEventTest +{ + [AvaloniaFact] + public async void Test() + { + UrsaWindow testWindow = new() + { + Content = new OverlayDialogHost() { HostId = "root" } + }; + testWindow.Show(); + DrawerCloseTestPopupControl level1 = new(); + OverlayDialog.ShowCustomModal(level1, new DrawerCloseTestPopupControlVM(), "root"); + level1.OpenPopup(); + var level2 = level1.Popup; + level2.OpenPopup(); + var level3 = level2.Popup; + level2.ClosePopup(); + await Task.Delay(TimeSpan.FromSeconds(1)); + Dispatcher.UIThread.RunJobs(); + Assert.True(level1.IsAttachedToVisualTree() + && level2.IsAttachedToVisualTree() + && level3.IsAttachedToVisualTree() is false); + + level1.ClosePopup(); + await Task.Delay(TimeSpan.FromSeconds(1)); + Dispatcher.UIThread.RunJobs(); + Assert.True(level1.IsAttachedToVisualTree() + && level2.IsAttachedToVisualTree() is false + && level3.IsAttachedToVisualTree() is false); + + level1.Close(); + await Task.Delay(TimeSpan.FromSeconds(1)); + Dispatcher.UIThread.RunJobs(); + Assert.False(level1.IsAttachedToVisualTree() + && level2.IsAttachedToVisualTree() + && level3.IsAttachedToVisualTree()); + + Assert.Equal(level1.LResult, level1.RResult); + Assert.Equal(level2.LResult, level2.RResult); + Assert.Equal(level3.LResult, level3.RResult); + + OverlayDialog.ShowCustomModal(level1, new DrawerCloseTestPopupControlVM(), "root"); + level1.OpenPopup(); + level2 = level1.Popup; + level2.OpenPopup(); + level3 = level2.Popup; + level3.OpenPopup(); + level1.Close(); + await Task.Delay(TimeSpan.FromSeconds(1)); + Dispatcher.UIThread.RunJobs(); + Assert.False(level1.IsAttachedToVisualTree() + && level2.IsAttachedToVisualTree() + && level3.IsAttachedToVisualTree()); + } +} \ No newline at end of file