From 5bdbd930d980999b249f3a7334b16fbd58b6e22e Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 16 Nov 2022 16:56:11 +0000 Subject: [PATCH 01/40] Add Refresh Container --- samples/ControlCatalog/MainView.xaml | 3 + .../Pages/RefreshContainerPage.axaml | 24 + .../Pages/RefreshContainerPage.axaml.cs | 36 ++ .../ViewModels/RefreshContainerViewModel.cs | 26 + src/Avalonia.Base/Input/Gestures.cs | 8 + .../Input/PullGestureEventArgs.cs | 43 ++ .../Input/PullGestureRecognizer.cs | 152 +++++ .../PullToRefresh/RefreshContainer.cs | 221 +++++++ .../PullToRefresh/RefreshInfoProvider.cs | 129 +++++ .../PullToRefresh/RefreshVisualizer.cs | 544 ++++++++++++++++++ ...ScrollViewerIRefreshInfoProviderAdapter.cs | 227 ++++++++ .../Accents/FluentControlResourcesDark.xaml | 6 +- .../Accents/FluentControlResourcesLight.xaml | 4 + .../Controls/FluentControls.xaml | 2 + .../Controls/RefreshContainer.xaml | 24 + .../Controls/RefreshVisualizer.xaml | 67 +++ .../Accents/BaseDark.xaml | 3 + .../Accents/BaseLight.xaml | 4 +- .../Controls/RefreshContainer.xaml | 24 + .../Controls/RefreshVisualizer.xaml | 72 +++ .../Controls/SimpleControls.xaml | 2 + 21 files changed, 1619 insertions(+), 2 deletions(-) create mode 100644 samples/ControlCatalog/Pages/RefreshContainerPage.axaml create mode 100644 samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs create mode 100644 samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs create mode 100644 src/Avalonia.Base/Input/PullGestureEventArgs.cs create mode 100644 src/Avalonia.Base/Input/PullGestureRecognizer.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs create mode 100644 src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml create mode 100644 src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml create mode 100644 src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml create mode 100644 src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index b5a09b5fbd..33ea8c0db0 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -132,6 +132,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml new file mode 100644 index 0000000000..0123251bd0 --- /dev/null +++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs new file mode 100644 index 0000000000..4c52179e00 --- /dev/null +++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public class RefreshContainerPage : UserControl + { + private RefreshContainerViewModel _viewModel; + + public RefreshContainerPage() + { + this.InitializeComponent(); + + _viewModel = new RefreshContainerViewModel(); + + DataContext = _viewModel; + } + + private async void RefreshContainerPage_RefreshRequested(object? sender, RefreshRequestedEventArgs e) + { + var deferral = e.GetRefreshCompletionDeferral(); + + await _viewModel.AddToTop(); + + deferral.Complete(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs b/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs new file mode 100644 index 0000000000..a78bc9e44a --- /dev/null +++ b/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs @@ -0,0 +1,26 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Threading.Tasks; +using Avalonia.Controls.Notifications; +using ControlCatalog.Pages; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class RefreshContainerViewModel : ViewModelBase + { + public ObservableCollection Items { get; } + + public RefreshContainerViewModel() + { + Items = new ObservableCollection(Enumerable.Range(1, 200).Select(i => $"Item {i}")); + } + + public async Task AddToTop() + { + await Task.Delay(1000); + Items.Insert(0, $"Item {200 - Items.Count}"); + } + } +} diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index 82ed96d982..acd78515cd 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -46,6 +46,14 @@ namespace Avalonia.Input private static readonly WeakReference s_lastPress = new WeakReference(null); private static Point s_lastPressPoint; + public static readonly RoutedEvent PullGestureEvent = + RoutedEvent.Register( + "PullGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PullGestureEndedEvent = + RoutedEvent.Register( + "PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); + static Gestures() { InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); diff --git a/src/Avalonia.Base/Input/PullGestureEventArgs.cs b/src/Avalonia.Base/Input/PullGestureEventArgs.cs new file mode 100644 index 0000000000..57ad24f7a3 --- /dev/null +++ b/src/Avalonia.Base/Input/PullGestureEventArgs.cs @@ -0,0 +1,43 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + public class PullGestureEventArgs : RoutedEventArgs + { + public int Id { get; } + public Vector Delta { get; } + public PullDirection PullDirection { get; } + + private static int _nextId = 1; + + public static int GetNextFreeId() => _nextId++; + + public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(Gestures.PullGestureEvent) + { + Id = id; + Delta = delta; + PullDirection = pullDirection; + } + } + + public class PullGestureEndedEventArgs : RoutedEventArgs + { + public int Id { get; } + public PullDirection PullDirection { get; } + + public PullGestureEndedEventArgs(int id, PullDirection pullDirection) : base(Gestures.PullGestureEndedEvent) + { + Id = id; + PullDirection = pullDirection; + } + } + + public enum PullDirection + { + TopToBottom, + BottomToTop, + LeftToRight, + RightToLeft + } +} diff --git a/src/Avalonia.Base/Input/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/PullGestureRecognizer.cs new file mode 100644 index 0000000000..bbbded44fa --- /dev/null +++ b/src/Avalonia.Base/Input/PullGestureRecognizer.cs @@ -0,0 +1,152 @@ +using Avalonia.Input.GestureRecognizers; + +namespace Avalonia.Input +{ + public class PullGestureRecognizer : StyledElement, IGestureRecognizer + { + private IInputElement? _target; + private IGestureRecognizerActionsDispatcher? _actions; + private Point _initialPosition; + private int _gestureId; + private IPointer? _tracking; + private PullDirection _pullDirection; + + /// + /// Defines the property. + /// + public static readonly DirectProperty PullDirectionProperty = + AvaloniaProperty.RegisterDirect( + nameof(PullDirection), + o => o.PullDirection, + (o, v) => o.PullDirection = v); + + public PullDirection PullDirection + { + get => _pullDirection; + set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value); + } + + public PullGestureRecognizer(PullDirection pullDirection) + { + PullDirection = pullDirection; + } + + public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) + { + _target = target; + _actions = actions; + + _target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble); + _target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble); + } + + private void OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + PointerPressed(e); + } + + private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + PointerReleased(e); + } + + public void PointerCaptureLost(IPointer pointer) + { + if (_tracking == pointer) + { + EndPull(); + } + } + + public void PointerMoved(PointerEventArgs e) + { + if (_tracking == e.Pointer) + { + var currentPosition = e.GetPosition(_target); + _actions!.Capture(e.Pointer, this); + + Vector delta = default; + switch (PullDirection) + { + case PullDirection.TopToBottom: + if (currentPosition.Y > _initialPosition.Y) + { + delta = new Vector(0, currentPosition.Y - _initialPosition.Y); + } + break; + case PullDirection.BottomToTop: + if (currentPosition.Y < _initialPosition.Y) + { + delta = new Vector(0, _initialPosition.Y - currentPosition.Y); + } + break; + case PullDirection.LeftToRight: + if (currentPosition.X > _initialPosition.X) + { + delta = new Vector(currentPosition.X - _initialPosition.X, 0); + } + break; + case PullDirection.RightToLeft: + if (currentPosition.X < _initialPosition.X) + { + delta = new Vector(_initialPosition.X - currentPosition.X, 0); + } + break; + } + + _target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection)); + } + } + + public void PointerPressed(PointerPressedEventArgs e) + { + if (_target != null && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + { + var position = e.GetPosition(_target); + + var canPull = false; + + var bounds = _target.Bounds; + + switch (PullDirection) + { + case PullDirection.TopToBottom: + canPull = position.Y < bounds.Height * 0.1; + break; + case PullDirection.BottomToTop: + canPull = position.Y > bounds.Height - (bounds.Height * 0.1); + break; + case PullDirection.LeftToRight: + canPull = position.X < bounds.Width * 0.1; + break; + case PullDirection.RightToLeft: + canPull = position.X > bounds.Width - (bounds.Width * 0.1); + break; + } + + if (canPull) + { + _gestureId = PullGestureEventArgs.GetNextFreeId(); + _tracking = e.Pointer; + _initialPosition = position; + } + } + } + + public void PointerReleased(PointerReleasedEventArgs e) + { + if (_tracking == e.Pointer) + { + EndPull(); + } + } + + private void EndPull() + { + _tracking = null; + _initialPosition = default; + + _target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection)); + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs new file mode 100644 index 0000000000..b882cf5a0f --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs @@ -0,0 +1,221 @@ +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.PullToRefresh; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + public class RefreshContainer : ContentControl + { + internal const int DefaultPullDimensionSize = 100; + + private readonly bool _hasDefaultRefreshInfoProviderAdapter; + + private ScrollViewerIRefreshInfoProviderAdapter _refreshInfoProviderAdapter; + private RefreshInfoProvider _refreshInfoProvider; + private IDisposable _visualizerSizeSubscription; + private Grid? _visualizerPresenter; + private RefreshVisualizer _refreshVisualizer; + + public static readonly RoutedEvent RefreshRequestedEvent = + RoutedEvent.Register(nameof(RefreshRequested), RoutingStrategies.Bubble); + + internal static readonly DirectProperty RefreshInfoProviderAdapterProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProviderAdapter), + (s) => s.RefreshInfoProviderAdapter, (s, o) => s.RefreshInfoProviderAdapter = o); + + public static readonly DirectProperty RefreshVisualizerProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizer), + s => s.RefreshVisualizer, (s, o) => s.RefreshVisualizer = o); + + public static readonly StyledProperty PullDirectionProperty = + AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); + + public ScrollViewerIRefreshInfoProviderAdapter RefreshInfoProviderAdapter + { + get => _refreshInfoProviderAdapter; set + { + SetAndRaise(RefreshInfoProviderAdapterProperty, ref _refreshInfoProviderAdapter, value); + } + } + + private bool _hasDefaultRefreshVisualizer; + + public RefreshVisualizer RefreshVisualizer + { + get => _refreshVisualizer; set + { + if (_refreshVisualizer != null) + { + _visualizerSizeSubscription?.Dispose(); + _refreshVisualizer.RefreshRequested -= Visualizer_RefreshRequested; + } + + SetAndRaise(RefreshVisualizerProperty, ref _refreshVisualizer, value); + } + } + + public PullDirection PullDirection + { + get => GetValue(PullDirectionProperty); + set => SetValue(PullDirectionProperty, value); + } + + public event EventHandler? RefreshRequested + { + add => AddHandler(RefreshRequestedEvent, value); + remove => RemoveHandler(RefreshRequestedEvent, value); + } + + public RefreshContainer() + { + _hasDefaultRefreshInfoProviderAdapter = true; + RefreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _visualizerPresenter = e.NameScope.Find("PART_RefreshVisualizerPresenter"); + + if (_refreshVisualizer == null) + { + _hasDefaultRefreshVisualizer = true; + RefreshVisualizer = new RefreshVisualizer(); + } + else + { + _hasDefaultRefreshVisualizer = false; + RaisePropertyChanged(RefreshVisualizerProperty, null, _refreshVisualizer); + } + + OnPullDirectionChanged(); + } + + private void OnVisualizerSizeChanged(Rect obj) + { + if (_hasDefaultRefreshInfoProviderAdapter) + { + RefreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); + } + } + + private void Visualizer_RefreshRequested(object? sender, RefreshRequestedEventArgs e) + { + var ev = new RefreshRequestedEventArgs(e.GetRefreshCompletionDeferral(), RefreshRequestedEvent); + RaiseEvent(ev); + ev.DecrementCount(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RefreshInfoProviderAdapterProperty) + { + if (_refreshInfoProvider != null) + { + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + } + else + { + if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null) + { + _refreshInfoProvider = RefreshInfoProviderAdapter.AdaptFromTree(this, _refreshVisualizer.Bounds.Size); + + if (_refreshInfoProvider != null) + { + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + } + } + } + } + else if (change.Property == RefreshVisualizerProperty) + { + if (_visualizerPresenter != null) + { + _visualizerPresenter.Children.Clear(); + if (_refreshVisualizer != null) + { + _visualizerPresenter.Children.Add(_refreshVisualizer); + } + } + + if (_refreshVisualizer != null) + { + _refreshVisualizer.RefreshRequested += Visualizer_RefreshRequested; + _visualizerSizeSubscription = _refreshVisualizer.GetObservable(Control.BoundsProperty).Subscribe(OnVisualizerSizeChanged); + } + } + else if (change.Property == PullDirectionProperty) + { + OnPullDirectionChanged(); + } + } + + private void OnPullDirectionChanged() + { + if (_visualizerPresenter != null) + { + switch (PullDirection) + { + case PullDirection.TopToBottom: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Top; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.TopToBottom; + _refreshVisualizer.Height = DefaultPullDimensionSize; + _refreshVisualizer.Width = double.NaN; + } + break; + case PullDirection.BottomToTop: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Bottom; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.BottomToTop; + _refreshVisualizer.Height = DefaultPullDimensionSize; + _refreshVisualizer.Width = double.NaN; + } + break; + case PullDirection.LeftToRight: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Left; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.LeftToRight; + _refreshVisualizer.Width = DefaultPullDimensionSize; + _refreshVisualizer.Height = double.NaN; + } + break; + case PullDirection.RightToLeft: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Right; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.RightToLeft; + _refreshVisualizer.Width = DefaultPullDimensionSize; + _refreshVisualizer.Height = double.NaN; + } + break; + } + + if (_hasDefaultRefreshInfoProviderAdapter && + _hasDefaultRefreshVisualizer && + _refreshVisualizer.Bounds.Height == DefaultPullDimensionSize && + _refreshVisualizer.Bounds.Width == DefaultPullDimensionSize) + { + RefreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); + } + } + } + + public void RequestRefresh() + { + _refreshVisualizer?.RequestRefresh(); + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs new file mode 100644 index 0000000000..3fc32cdd08 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs @@ -0,0 +1,129 @@ +using System; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Controls.PullToRefresh +{ + public class RefreshInfoProvider : Interactive + { + internal const double ExecutionRatio = 0.8; + + private readonly PullDirection _refreshPullDirection; + private readonly Size _refreshVisualizerSize; + + private readonly Visual _visual; + private bool _isInteractingForRefresh; + private double _interactionRatio; + private bool _entered; + + public DirectProperty IsInteractingForRefreshProperty = + AvaloniaProperty.RegisterDirect(nameof(IsInteractingForRefresh), + s => s.IsInteractingForRefresh, (s, o) => s.IsInteractingForRefresh = o); + + public DirectProperty InteractionRatioProperty = + AvaloniaProperty.RegisterDirect(nameof(InteractionRatio), + s => s.InteractionRatio, (s, o) => s.InteractionRatio = o); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent RefreshStartedEvent = + RoutedEvent.Register(nameof(RefreshStarted), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent RefreshCompletedEvent = + RoutedEvent.Register(nameof(RefreshCompleted), RoutingStrategies.Bubble); + + public bool PeekingMode { get; internal set; } + + public bool IsInteractingForRefresh + { + get => _isInteractingForRefresh; internal set + { + var isInteractingForRefresh = value && !PeekingMode; + + if (isInteractingForRefresh != _isInteractingForRefresh) + { + SetAndRaise(IsInteractingForRefreshProperty, ref _isInteractingForRefresh, isInteractingForRefresh); + } + } + } + + public double InteractionRatio + { + get => _interactionRatio; + set + { + SetAndRaise(InteractionRatioProperty, ref _interactionRatio, value); + } + } + + internal Visual Visual => _visual; + + public event EventHandler RefreshStarted + { + add => AddHandler(RefreshStartedEvent, value); + remove => RemoveHandler(RefreshStartedEvent, value); + } + + public event EventHandler RefreshCompleted + { + add => AddHandler(RefreshCompletedEvent, value); + remove => RemoveHandler(RefreshCompletedEvent, value); + } + + internal void InteractingStateEntered(object sender, PullGestureEventArgs e) + { + if (!_entered) + { + IsInteractingForRefresh = true; + _entered = true; + } + + ValuesChanged(e.Delta); + } + + internal void InteractingStateExited(object sender, PullGestureEndedEventArgs e) + { + IsInteractingForRefresh = false; + _entered = false; + + ValuesChanged(default); + } + + + public RefreshInfoProvider(PullDirection refreshPullDirection, Size refreshVIsualizerSize, Visual visual) + { + _refreshPullDirection = refreshPullDirection; + _refreshVisualizerSize = refreshVIsualizerSize; + _visual = visual; + } + + public void OnRefreshStarted() + { + RaiseEvent(new RoutedEventArgs(RefreshStartedEvent)); + } + + public void OnRefreshCompleted() + { + RaiseEvent(new RoutedEventArgs(RefreshCompletedEvent)); + } + + internal void ValuesChanged(Vector value) + { + switch (_refreshPullDirection) + { + case PullDirection.TopToBottom: + case PullDirection.BottomToTop: + InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.Y / _refreshVisualizerSize.Height); + break; + case PullDirection.LeftToRight: + case PullDirection.RightToLeft: + InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.X / _refreshVisualizerSize.Width); + break; + } + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs new file mode 100644 index 0000000000..81c613443d --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -0,0 +1,544 @@ +using System; +using System.Reactive.Linq; +using System.Threading; +using Avalonia.Animation; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.PullToRefresh; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + public class RefreshVisualizer : ContentControl + { + private const int DefaultIndicatorSize = 24; + private const double MinimumIndicatorOpacity = 0.4; + private const string ArrowPathData = "M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z"; + private double _executingRatio = 0.8; + + private RotateTransform _visualizerRotateTransform; + private TranslateTransform _contentTranslateTransform; + private RefreshVisualizerState _refreshVisualizerState; + private RefreshInfoProvider _refreshInfoProvider; + private IDisposable _isInteractingSubscription; + private IDisposable _interactionRatioSubscription; + private bool _isInteractingForRefresh; + private Grid? _root; + private Control _content; + private RefreshVisualizerOrientation _orientation; + private float _startingRotationAngle; + private double _interactionRatio; + + private bool IsPullDirectionVertical => PullDirection == PullDirection.TopToBottom || PullDirection == PullDirection.BottomToTop; + private bool IsPullDirectionFar => PullDirection == PullDirection.BottomToTop || PullDirection == PullDirection.RightToLeft; + + public static readonly StyledProperty PullDirectionProperty = + AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); + public static readonly RoutedEvent RefreshRequestedEvent = + RoutedEvent.Register(nameof(RefreshRequested), RoutingStrategies.Bubble); + + public static readonly DirectProperty RefreshVisualizerStateProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizerState), + s => s.RefreshVisualizerState); + + public static readonly DirectProperty OrientationProperty = + AvaloniaProperty.RegisterDirect(nameof(Orientation), + s => s.Orientation, (s, o) => s.Orientation = o); + + public DirectProperty RefreshInfoProviderProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProvider), + s => s.RefreshInfoProvider, (s, o) => s.RefreshInfoProvider = o); + + public RefreshVisualizerState RefreshVisualizerState + { + get + { + return _refreshVisualizerState; + } + private set + { + SetAndRaise(RefreshVisualizerStateProperty, ref _refreshVisualizerState, value); + UpdateContent(); + } + } + + public RefreshVisualizerOrientation Orientation + { + get + { + return _orientation; + } + set + { + SetAndRaise(OrientationProperty, ref _orientation, value); + } + } + + internal PullDirection PullDirection + { + get => GetValue(PullDirectionProperty); + set + { + SetValue(PullDirectionProperty, value); + + OnOrientationChanged(); + + UpdateContent(); + } + } + + public RefreshInfoProvider RefreshInfoProvider + { + get => _refreshInfoProvider; internal set + { + if (_refreshInfoProvider != null) + { + _refreshInfoProvider.RenderTransform = null; + } + SetAndRaise(RefreshInfoProviderProperty, ref _refreshInfoProvider, value); + } + } + + public event EventHandler? RefreshRequested + { + add => AddHandler(RefreshRequestedEvent, value); + remove => RemoveHandler(RefreshRequestedEvent, value); + } + + public RefreshVisualizer() + { + _visualizerRotateTransform = new RotateTransform(); + _contentTranslateTransform = new TranslateTransform(); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _root = e.NameScope.Find("PART_Root"); + + if (_root != null) + { + if (_content == null) + { + Content = new PathIcon() + { + Data = PathGeometry.Parse(ArrowPathData), + Height = DefaultIndicatorSize, + Width = DefaultIndicatorSize + }; + } + else + { + RaisePropertyChanged(ContentProperty, null, Content); + } + } + + OnOrientationChanged(); + + UpdateContent(); + } + + private void UpdateContent() + { + if (_content != null) + { + switch (RefreshVisualizerState) + { + case RefreshVisualizerState.Idle: + _content.Classes.Remove("refreshing"); + _root.Classes.Remove("pending"); + _content.RenderTransform = _visualizerRotateTransform; + _content.Opacity = MinimumIndicatorOpacity; + _visualizerRotateTransform.Angle = _startingRotationAngle; + _contentTranslateTransform.X = 0; + _contentTranslateTransform.Y = 0; + break; + case RefreshVisualizerState.Interacting: + _content.Classes.Remove("refreshing"); + _root.Classes.Remove("pending"); + _content.RenderTransform = _visualizerRotateTransform; + _content.Opacity = MinimumIndicatorOpacity; + _visualizerRotateTransform.Angle = _startingRotationAngle + (_interactionRatio * 360); + _content.Height = DefaultIndicatorSize; + _content.Width = DefaultIndicatorSize; + if (IsPullDirectionVertical) + { + _contentTranslateTransform.X = 0; + _contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; + } + else + { + _contentTranslateTransform.Y = 0; + _contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; + } + break; + case RefreshVisualizerState.Pending: + _content.Classes.Remove("refreshing"); + _content.Opacity = 1; + _content.RenderTransform = _visualizerRotateTransform; + if (IsPullDirectionVertical) + { + _contentTranslateTransform.X = 0; + _contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; + } + else + { + _contentTranslateTransform.Y = 0; + _contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; + } + + _root.Classes.Add("pending"); + break; + case RefreshVisualizerState.Refreshing: + _root.Classes.Remove("pending"); + _content.Classes.Add("refreshing"); + _content.Opacity = 1; + _content.Height = DefaultIndicatorSize; + _content.Width = DefaultIndicatorSize; + break; + case RefreshVisualizerState.Peeking: + _root.Classes.Remove("pending"); + _content.Opacity = 1; + _visualizerRotateTransform.Angle += _startingRotationAngle; + break; + } + } + } + + public void RequestRefresh() + { + RefreshVisualizerState = RefreshVisualizerState.Refreshing; + RefreshInfoProvider?.OnRefreshStarted(); + + RaiseRefreshRequested(); + } + + private void RefreshCompleted() + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + + RefreshInfoProvider?.OnRefreshCompleted(); + } + + private void RaiseRefreshRequested() + { + var refreshArgs = new RefreshRequestedEventArgs(RefreshCompleted, RefreshRequestedEvent); + + refreshArgs.IncrementCount(); + + RaiseEvent(refreshArgs); + + refreshArgs.DecrementCount(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RefreshInfoProviderProperty) + { + OnRefreshInfoProviderChanged(); + } + else if (change.Property == ContentProperty) + { + if (_root != null) + { + if (_content == null) + { + _content = new PathIcon() + { + Data = PathGeometry.Parse(ArrowPathData), + Height = DefaultIndicatorSize, + Width = DefaultIndicatorSize + }; + + var transformGroup = new TransformGroup(); + transformGroup.Children.Add(_visualizerRotateTransform); + + _content.RenderTransform = _visualizerRotateTransform; + _root.RenderTransform = _contentTranslateTransform; + + var transition = new Transitions + { + new DoubleTransition() + { + Property = OpacityProperty, + Duration = TimeSpan.FromSeconds(0.5) + }, + }; + + _content.Transitions = transition; + } + + var scalingGrid = new Grid(); + scalingGrid.VerticalAlignment = Layout.VerticalAlignment.Center; + scalingGrid.HorizontalAlignment = Layout.HorizontalAlignment.Center; + scalingGrid.RenderTransform = new ScaleTransform(); + + scalingGrid.Children.Add(_content); + + _root.Children.Insert(0, scalingGrid); + _content.VerticalAlignment = Layout.VerticalAlignment.Center; + _content.HorizontalAlignment = Layout.HorizontalAlignment.Center; + } + + UpdateContent(); + } + else if (change.Property == OrientationProperty) + { + OnOrientationChanged(); + + UpdateContent(); + } + else if (change.Property == BoundsProperty) + { + if (_content != null) + { + var parent = _content.Parent as Control; + switch (PullDirection) + { + case PullDirection.TopToBottom: + parent.Margin = new Thickness(0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0); + break; + case PullDirection.BottomToTop: + parent.Margin = new Thickness(0, 0, 0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize)); + break; + case PullDirection.LeftToRight: + parent.Margin = new Thickness(-Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0, 0); + break; + case PullDirection.RightToLeft: + parent.Margin = new Thickness(0, 0, -Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0); + break; + } + } + } + } + + private void OnOrientationChanged() + { + switch (_orientation) + { + case RefreshVisualizerOrientation.Auto: + switch (PullDirection) + { + case PullDirection.TopToBottom: + case PullDirection.BottomToTop: + _startingRotationAngle = 0.0f; + break; + case PullDirection.LeftToRight: + _startingRotationAngle = 270; + break; + case PullDirection.RightToLeft: + _startingRotationAngle = 90; + break; + } + break; + case RefreshVisualizerOrientation.Normal: + _startingRotationAngle = 0.0f; + break; + case RefreshVisualizerOrientation.Rotate90DegreesCounterclockwise: + _startingRotationAngle = 270; + break; + case RefreshVisualizerOrientation.Rotate270DegreesCounterclockwise: + _startingRotationAngle = 90; + break; + } + } + + private void OnRefreshInfoProviderChanged() + { + _isInteractingSubscription?.Dispose(); + _isInteractingSubscription = null; + _interactionRatioSubscription?.Dispose(); + _interactionRatioSubscription = null; + + if (_refreshInfoProvider != null) + { + _isInteractingSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty) + .Subscribe(InteractingForRefreshObserver); + + _interactionRatioSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty) + .Subscribe(InteractionRatioObserver); + + var visual = _refreshInfoProvider.Visual; + visual.RenderTransform = _contentTranslateTransform; + + _executingRatio = RefreshInfoProvider.ExecutionRatio; + } + else + { + _executingRatio = 1; + } + } + + private void InteractionRatioObserver(double obj) + { + var wasAtZero = _interactionRatio == 0.0; + _interactionRatio = obj; + + if (_isInteractingForRefresh) + { + if (RefreshVisualizerState == RefreshVisualizerState.Idle) + { + if (wasAtZero) + { + if (_interactionRatio > _executingRatio) + { + RefreshVisualizerState = RefreshVisualizerState.Pending; + } + else if (_interactionRatio > 0) + { + RefreshVisualizerState = RefreshVisualizerState.Interacting; + } + } + else if (_interactionRatio > 0) + { + RefreshVisualizerState = RefreshVisualizerState.Peeking; + } + } + else if (RefreshVisualizerState == RefreshVisualizerState.Interacting) + { + if (_interactionRatio <= 0) + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + } + else if (_interactionRatio > _executingRatio) + { + RefreshVisualizerState = RefreshVisualizerState.Pending; + } + else + { + UpdateContent(); + } + } + else if (RefreshVisualizerState == RefreshVisualizerState.Pending) + { + if (_interactionRatio <= _executingRatio) + { + RefreshVisualizerState = RefreshVisualizerState.Interacting; + } + else if (_interactionRatio <= 0) + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + } + else + { + UpdateContent(); + } + } + } + else + { + if (RefreshVisualizerState != RefreshVisualizerState.Refreshing) + { + if (_interactionRatio > 0) + { + RefreshVisualizerState = RefreshVisualizerState.Peeking; + } + else + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + } + } + } + } + + private void InteractingForRefreshObserver(bool obj) + { + _isInteractingForRefresh = obj; + + if (!_isInteractingForRefresh) + { + switch (_refreshVisualizerState) + { + case RefreshVisualizerState.Pending: + RequestRefresh(); + break; + case RefreshVisualizerState.Refreshing: + // We don't want to interrupt a currently executing refresh. + break; + default: + RefreshVisualizerState = RefreshVisualizerState.Idle; + break; + } + } + } + } + + public enum RefreshVisualizerState + { + Idle, + Peeking, + Interacting, + Pending, + Refreshing + } + + public enum RefreshVisualizerOrientation + { + Auto, + Normal, + Rotate90DegreesCounterclockwise, + Rotate270DegreesCounterclockwise + } + + public class RefreshRequestedEventArgs : RoutedEventArgs + { + private RefreshCompletionDeferral _refreshCompletionDeferral; + + public RefreshCompletionDeferral GetRefreshCompletionDeferral() + { + return _refreshCompletionDeferral.Get(); + } + + public RefreshRequestedEventArgs(Action deferredAction, RoutedEvent? routedEvent) : base(routedEvent) + { + _refreshCompletionDeferral = new RefreshCompletionDeferral(deferredAction); + } + + public RefreshRequestedEventArgs(RefreshCompletionDeferral completionDeferral, RoutedEvent? routedEvent) : base(routedEvent) + { + _refreshCompletionDeferral = completionDeferral; + } + + internal void IncrementCount() + { + _refreshCompletionDeferral?.Get(); + } + + internal void DecrementCount() + { + _refreshCompletionDeferral?.Complete(); + } + } + + public class RefreshCompletionDeferral + { + private Action _deferredAction; + private int _deferCount; + + public RefreshCompletionDeferral(Action deferredAction) + { + _deferredAction = deferredAction; + } + + public void Complete() + { + Interlocked.Decrement(ref _deferCount); + + if (_deferCount == 0) + { + _deferredAction?.Invoke(); + } + } + + public RefreshCompletionDeferral Get() + { + Interlocked.Increment(ref _deferCount); + + return this; + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs new file mode 100644 index 0000000000..0b0c8c99b2 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs @@ -0,0 +1,227 @@ +using System; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.PullToRefresh +{ + public class ScrollViewerIRefreshInfoProviderAdapter + { + private const int MaxSearchDepth = 10; + private const int InitialOffsetThreshold = 1; + + private PullDirection _refreshPullDirection; + private ScrollViewer _scrollViewer; + private RefreshInfoProvider _refreshInfoProvider; + private PullGestureRecognizer _pullGestureRecognizer; + private InputElement? _interactionSource; + private bool _isVisualizerInteractionSourceAttached; + + public ScrollViewerIRefreshInfoProviderAdapter(PullDirection pullDirection) + { + _refreshPullDirection = pullDirection; + } + + public RefreshInfoProvider AdaptFromTree(IVisual root, Size refreshVIsualizerSize) + { + if (root is ScrollViewer scrollViewer) + { + return Adapt(scrollViewer, refreshVIsualizerSize); + } + else + { + int depth = 0; + while (depth < MaxSearchDepth) + { + var scroll = AdaptFromTreeRecursiveHelper(root, depth); + + if (scroll != null) + { + return Adapt(scroll, refreshVIsualizerSize); + } + + depth++; + } + } + + ScrollViewer AdaptFromTreeRecursiveHelper(IVisual root, int depth) + { + if (depth == 0) + { + foreach (var child in root.VisualChildren) + { + if (child is ScrollViewer viewer) + { + return viewer; + } + } + } + else + { + foreach (var child in root.VisualChildren) + { + var viewer = AdaptFromTreeRecursiveHelper(child, depth - 1); + if (viewer != null) + { + return viewer; + } + } + } + + return null; + } + + return null; + } + + public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size refreshVIsualizerSize) + { + if (adaptee == null) + { + throw new ArgumentNullException(nameof(adaptee), "Adaptee cannot be null"); + } + + if (_scrollViewer != null) + { + CleanUpScrollViewer(); + } + + if (_refreshInfoProvider != null && _interactionSource != null) + { + _interactionSource.RemoveHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource.RemoveHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + } + + _refreshInfoProvider = null; + _scrollViewer = adaptee; + + if (_scrollViewer.Content == null) + { + throw new ArgumentException(nameof(adaptee), "Adaptee's content property cannot be null."); + } + + var content = adaptee.Content as Visual; + + if (content == null) + { + throw new ArgumentException(nameof(adaptee), "Adaptee's content property must be a Visual"); + } + + if (content.GetVisualParent() == null) + { + _scrollViewer.Loaded += ScrollViewer_Loaded; + } + else + { + ScrollViewer_Loaded(null, null); + + if (content.Parent is not InputElement) + { + throw new ArgumentException(nameof(adaptee), "Adaptee's content's parent must be a InputElement"); + } + } + + _refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, content); + + _pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection); + + if (_interactionSource != null) + { + _interactionSource.GestureRecognizers.Add(_pullGestureRecognizer); + _interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + _isVisualizerInteractionSourceAttached = true; + } + + _scrollViewer.PointerPressed += ScrollViewer_PointerPressed; + _scrollViewer.PointerReleased += ScrollViewer_PointerReleased; + _scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged; + + return _refreshInfoProvider; + } + + private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) + { + if (_isVisualizerInteractionSourceAttached && _refreshInfoProvider != null && _refreshInfoProvider.IsInteractingForRefresh) + { + if (!IsWithinOffsetThreashold()) + { + _refreshInfoProvider.IsInteractingForRefresh = false; + } + } + } + + private void ScrollViewer_Loaded(object sender, RoutedEventArgs e) + { + var content = _scrollViewer.Content as Visual; + if (content == null) + { + throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual"); + } + + if (content.Parent is not InputElement) + { + throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content parent must be an InputElement"); + } + + MakeInteractionSource(content.Parent as InputElement); + + _scrollViewer.Loaded -= ScrollViewer_Loaded; + } + + private void MakeInteractionSource(InputElement element) + { + _interactionSource = element; + + if (_pullGestureRecognizer != null) + { + element.GestureRecognizers.Add(_pullGestureRecognizer); + _interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + _isVisualizerInteractionSourceAttached = true; + } + } + + private void ScrollViewer_PointerReleased(object sender, PointerReleasedEventArgs e) + { + if (_refreshInfoProvider != null) + { + _refreshInfoProvider.IsInteractingForRefresh = false; + } + } + + private void ScrollViewer_PointerPressed(object sender, PointerPressedEventArgs e) + { + _refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold(); + } + + private bool IsWithinOffsetThreashold() + { + if (_scrollViewer != null) + { + var offset = _scrollViewer.Offset; + + switch (_refreshPullDirection) + { + case PullDirection.TopToBottom: + return offset.Y < InitialOffsetThreshold; + case PullDirection.LeftToRight: + return offset.X < InitialOffsetThreshold; + case PullDirection.RightToLeft: + return offset.X > _scrollViewer.Extent.Width - _scrollViewer.Viewport.Width - InitialOffsetThreshold; + case PullDirection.BottomToTop: + return offset.Y > _scrollViewer.Extent.Height - _scrollViewer.Viewport.Height - InitialOffsetThreshold; + } + } + + return false; + } + + private void CleanUpScrollViewer() + { + _scrollViewer.PointerPressed -= ScrollViewer_PointerPressed; + _scrollViewer.PointerReleased -= ScrollViewer_PointerReleased; + _scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged; + } + } +} diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 7e3c8673f5..d9be545801 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -637,6 +637,10 @@ - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 7917315e19..e80b137b2d 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -633,5 +633,9 @@ + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 5383aa3180..5d38b055b3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -69,6 +69,8 @@ + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml b/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml new file mode 100644 index 0000000000..97002c25bd --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml new file mode 100644 index 0000000000..388ca814f8 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml index 1843abebfd..c748775a12 100644 --- a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml @@ -33,6 +33,9 @@ + + + diff --git a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml index 6247815303..43acb16be5 100644 --- a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml @@ -33,6 +33,8 @@ - + + + diff --git a/src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml b/src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml new file mode 100644 index 0000000000..326b217068 --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml new file mode 100644 index 0000000000..0b12fe133d --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 4aefa0136c..4bad556338 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -65,6 +65,8 @@ + + From 68fd64f1640b657eb3f15987ff9e077eb7f366ac Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 17 Nov 2022 15:29:39 +0000 Subject: [PATCH 02/40] Use Composition Api for animation --- .../Pages/RefreshContainerPage.axaml | 2 + .../Input/PullGestureRecognizer.cs | 2 +- .../PullToRefresh/RefreshContainer.cs | 48 +-- .../PullToRefresh/RefreshInfoProvider.cs | 13 +- .../PullToRefresh/RefreshVisualizer.cs | 282 +++++++++++------- ...ScrollViewerIRefreshInfoProviderAdapter.cs | 91 ++++-- .../Controls/RefreshVisualizer.xaml | 32 +- 7 files changed, 277 insertions(+), 193 deletions(-) diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml index 0123251bd0..d467d14427 100644 --- a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml +++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml @@ -2,9 +2,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:viewModels="using:ControlCatalog.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:DataType="viewModels:RefreshContainerViewModel" x:Class="ControlCatalog.Pages.RefreshContainerPage"> RefreshRequestedEvent = RoutedEvent.Register(nameof(RefreshRequested), RoutingStrategies.Bubble); - internal static readonly DirectProperty RefreshInfoProviderAdapterProperty = - AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProviderAdapter), + internal static readonly DirectProperty RefreshInfoProviderAdapterProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProviderAdapter), (s) => s.RefreshInfoProviderAdapter, (s, o) => s.RefreshInfoProviderAdapter = o); - public static readonly DirectProperty RefreshVisualizerProperty = - AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizer), + public static readonly DirectProperty RefreshVisualizerProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizer), s => s.RefreshVisualizer, (s, o) => s.RefreshVisualizer = o); public static readonly StyledProperty PullDirectionProperty = AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); - public ScrollViewerIRefreshInfoProviderAdapter RefreshInfoProviderAdapter + public ScrollViewerIRefreshInfoProviderAdapter? RefreshInfoProviderAdapter { get => _refreshInfoProviderAdapter; set { @@ -42,7 +42,7 @@ namespace Avalonia.Controls private bool _hasDefaultRefreshVisualizer; - public RefreshVisualizer RefreshVisualizer + public RefreshVisualizer? RefreshVisualizer { get => _refreshVisualizer; set { @@ -88,7 +88,7 @@ namespace Avalonia.Controls else { _hasDefaultRefreshVisualizer = false; - RaisePropertyChanged(RefreshVisualizerProperty, null, _refreshVisualizer); + RaisePropertyChanged(RefreshVisualizerProperty, default, _refreshVisualizer); } OnPullDirectionChanged(); @@ -115,19 +115,23 @@ namespace Avalonia.Controls if (change.Property == RefreshInfoProviderAdapterProperty) { - if (_refreshInfoProvider != null) - { - _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; - } - else + if (_refreshVisualizer != null) { - if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null) + if (_refreshInfoProvider != null) { - _refreshInfoProvider = RefreshInfoProviderAdapter.AdaptFromTree(this, _refreshVisualizer.Bounds.Size); - - if (_refreshInfoProvider != null) + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + } + else + { + if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null) { - _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + _refreshInfoProvider = RefreshInfoProviderAdapter?.AdaptFromTree(this, _refreshVisualizer.Bounds.Size); + + if (_refreshInfoProvider != null) + { + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + RefreshInfoProviderAdapter?.SetAnimations(_refreshVisualizer); + } } } } @@ -157,7 +161,7 @@ namespace Avalonia.Controls private void OnPullDirectionChanged() { - if (_visualizerPresenter != null) + if (_visualizerPresenter != null && _refreshVisualizer != null) { switch (PullDirection) { diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs index 3fc32cdd08..506b48f6e2 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Rendering.Composition; namespace Avalonia.Controls.PullToRefresh { @@ -11,7 +12,7 @@ namespace Avalonia.Controls.PullToRefresh private readonly PullDirection _refreshPullDirection; private readonly Size _refreshVisualizerSize; - private readonly Visual _visual; + private readonly CompositionVisual? _visual; private bool _isInteractingForRefresh; private double _interactionRatio; private bool _entered; @@ -60,7 +61,7 @@ namespace Avalonia.Controls.PullToRefresh } } - internal Visual Visual => _visual; + internal CompositionVisual? Visual => _visual; public event EventHandler RefreshStarted { @@ -74,7 +75,7 @@ namespace Avalonia.Controls.PullToRefresh remove => RemoveHandler(RefreshCompletedEvent, value); } - internal void InteractingStateEntered(object sender, PullGestureEventArgs e) + internal void InteractingStateEntered(object? sender, PullGestureEventArgs e) { if (!_entered) { @@ -85,7 +86,7 @@ namespace Avalonia.Controls.PullToRefresh ValuesChanged(e.Delta); } - internal void InteractingStateExited(object sender, PullGestureEndedEventArgs e) + internal void InteractingStateExited(object? sender, PullGestureEndedEventArgs e) { IsInteractingForRefresh = false; _entered = false; @@ -94,10 +95,10 @@ namespace Avalonia.Controls.PullToRefresh } - public RefreshInfoProvider(PullDirection refreshPullDirection, Size refreshVIsualizerSize, Visual visual) + public RefreshInfoProvider(PullDirection refreshPullDirection, Size? refreshVIsualizerSize, CompositionVisual? visual) { _refreshPullDirection = refreshPullDirection; - _refreshVisualizerSize = refreshVIsualizerSize; + _refreshVisualizerSize = refreshVIsualizerSize ?? default; _visual = visual; } diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index 81c613443d..5d0eee7478 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -1,4 +1,5 @@ using System; +using System.Numerics; using System.Reactive.Linq; using System.Threading; using Avalonia.Animation; @@ -7,25 +8,24 @@ using Avalonia.Controls.PullToRefresh; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Rendering.Composition; namespace Avalonia.Controls { public class RefreshVisualizer : ContentControl { private const int DefaultIndicatorSize = 24; - private const double MinimumIndicatorOpacity = 0.4; + private const float MinimumIndicatorOpacity = 0.4f; private const string ArrowPathData = "M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z"; private double _executingRatio = 0.8; - private RotateTransform _visualizerRotateTransform; - private TranslateTransform _contentTranslateTransform; private RefreshVisualizerState _refreshVisualizerState; - private RefreshInfoProvider _refreshInfoProvider; - private IDisposable _isInteractingSubscription; - private IDisposable _interactionRatioSubscription; + private RefreshInfoProvider? _refreshInfoProvider; + private IDisposable? _isInteractingSubscription; + private IDisposable? _interactionRatioSubscription; private bool _isInteractingForRefresh; private Grid? _root; - private Control _content; + private Control? _content; private RefreshVisualizerOrientation _orientation; private float _startingRotationAngle; private double _interactionRatio; @@ -46,9 +46,11 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect(nameof(Orientation), s => s.Orientation, (s, o) => s.Orientation = o); - public DirectProperty RefreshInfoProviderProperty = - AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProvider), + public DirectProperty RefreshInfoProviderProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProvider), s => s.RefreshInfoProvider, (s, o) => s.RefreshInfoProvider = o); + private Vector3 _defaultOffset; + private bool _played; public RefreshVisualizerState RefreshVisualizerState { @@ -88,7 +90,7 @@ namespace Avalonia.Controls } } - public RefreshInfoProvider RefreshInfoProvider + public RefreshInfoProvider? RefreshInfoProvider { get => _refreshInfoProvider; internal set { @@ -106,20 +108,17 @@ namespace Avalonia.Controls remove => RemoveHandler(RefreshRequestedEvent, value); } - public RefreshVisualizer() - { - _visualizerRotateTransform = new RotateTransform(); - _contentTranslateTransform = new TranslateTransform(); - } - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); + this.ClipToBounds = false; + _root = e.NameScope.Find("PART_Root"); if (_root != null) { + _content = Content as Control; if (_content == null) { Content = new PathIcon() @@ -140,69 +139,113 @@ namespace Avalonia.Controls UpdateContent(); } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + UpdateContent(); + } + private void UpdateContent() { - if (_content != null) + if (_content != null && _root != null) { - switch (RefreshVisualizerState) + var root = _root; + var visual = _refreshInfoProvider?.Visual; + var contentVisual = ElementComposition.GetElementVisual(_content); + var visualizerVisual = ElementComposition.GetElementVisual(this); + if (visual != null && contentVisual != null && visualizerVisual != null) { - case RefreshVisualizerState.Idle: - _content.Classes.Remove("refreshing"); - _root.Classes.Remove("pending"); - _content.RenderTransform = _visualizerRotateTransform; - _content.Opacity = MinimumIndicatorOpacity; - _visualizerRotateTransform.Angle = _startingRotationAngle; - _contentTranslateTransform.X = 0; - _contentTranslateTransform.Y = 0; - break; - case RefreshVisualizerState.Interacting: - _content.Classes.Remove("refreshing"); - _root.Classes.Remove("pending"); - _content.RenderTransform = _visualizerRotateTransform; - _content.Opacity = MinimumIndicatorOpacity; - _visualizerRotateTransform.Angle = _startingRotationAngle + (_interactionRatio * 360); - _content.Height = DefaultIndicatorSize; - _content.Width = DefaultIndicatorSize; - if (IsPullDirectionVertical) - { - _contentTranslateTransform.X = 0; - _contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; - } - else - { - _contentTranslateTransform.Y = 0; - _contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; - } - break; - case RefreshVisualizerState.Pending: - _content.Classes.Remove("refreshing"); - _content.Opacity = 1; - _content.RenderTransform = _visualizerRotateTransform; - if (IsPullDirectionVertical) - { - _contentTranslateTransform.X = 0; - _contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; - } - else - { - _contentTranslateTransform.Y = 0; - _contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; - } - - _root.Classes.Add("pending"); - break; - case RefreshVisualizerState.Refreshing: - _root.Classes.Remove("pending"); - _content.Classes.Add("refreshing"); - _content.Opacity = 1; - _content.Height = DefaultIndicatorSize; - _content.Width = DefaultIndicatorSize; - break; - case RefreshVisualizerState.Peeking: - _root.Classes.Remove("pending"); - _content.Opacity = 1; - _visualizerRotateTransform.Angle += _startingRotationAngle; - break; + contentVisual.CenterPoint = new Vector3((float)(_content.Bounds.Width / 2), (float)(_content.Bounds.Height / 2), 0); + switch (RefreshVisualizerState) + { + case RefreshVisualizerState.Idle: + _played = false; + contentVisual.Opacity = MinimumIndicatorOpacity; + contentVisual.RotationAngle = (float)(_startingRotationAngle * Math.PI / 180f); + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, 0, 0) : + new Vector3(0, visualizerVisual.Offset.Y, 0); + contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f); + visual.Offset = default; + break; + case RefreshVisualizerState.Interacting: + _played = false; + contentVisual.Opacity = MinimumIndicatorOpacity; + contentVisual.RotationAngle = (float)((_startingRotationAngle + (_interactionRatio * 360)) * Math.PI / 180f); + Vector3 offset = default; + if (IsPullDirectionVertical) + { + offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + } + else + { + offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); + } + visual.Offset = offset; + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f); + break; + case RefreshVisualizerState.Pending: + contentVisual.Opacity = 1; + contentVisual.RotationAngle = (float)((_startingRotationAngle + 360) * Math.PI / 180f); + if (IsPullDirectionVertical) + { + offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + } + else + { + offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); + } + visual.Offset = offset; + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + + if (!_played) + { + _played = true; + var scaleAnimation = contentVisual.Compositor!.CreateVector3KeyFrameAnimation(); + scaleAnimation.Target = "Scale"; + scaleAnimation.InsertKeyFrame(0.5f, new Vector3(1.5f, 1.5f, 1)); + scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1)); + scaleAnimation.Duration = TimeSpan.FromSeconds(0.3); + + contentVisual.StartAnimation("Scale", scaleAnimation); + } + break; + case RefreshVisualizerState.Refreshing: + var rotateAnimation = contentVisual.Compositor!.CreateScalarKeyFrameAnimation(); + rotateAnimation.Target = "RotationAngle"; + rotateAnimation.InsertKeyFrame(0, (float)(0)); + rotateAnimation.InsertKeyFrame(0.5f, (float)(Math.PI)); + rotateAnimation.InsertKeyFrame(1, (float)(2 * Math.PI)); + rotateAnimation.Duration = TimeSpan.FromSeconds(1); + rotateAnimation.IterationCount = 1000; + + contentVisual.StartAnimation("RotationAngle", rotateAnimation); + contentVisual.Opacity = 1; + contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f); + if (IsPullDirectionVertical) + { + offset = new Vector3(0, (float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + } + else + { + offset = new Vector3((float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); + } + visual.Offset = offset; + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + break; + case RefreshVisualizerState.Peeking: + contentVisual.Opacity = 1; + contentVisual.RotationAngle = (float)(_startingRotationAngle * Math.PI / 180f); + break; + } } } } @@ -254,12 +297,6 @@ namespace Avalonia.Controls Width = DefaultIndicatorSize }; - var transformGroup = new TransformGroup(); - transformGroup.Children.Add(_visualizerRotateTransform); - - _content.RenderTransform = _visualizerRotateTransform; - _root.RenderTransform = _contentTranslateTransform; - var transition = new Transitions { new DoubleTransition() @@ -270,16 +307,42 @@ namespace Avalonia.Controls }; _content.Transitions = transition; - } - var scalingGrid = new Grid(); - scalingGrid.VerticalAlignment = Layout.VerticalAlignment.Center; - scalingGrid.HorizontalAlignment = Layout.HorizontalAlignment.Center; - scalingGrid.RenderTransform = new ScaleTransform(); - - scalingGrid.Children.Add(_content); + _content.Loaded += (s, e) => + { + var composition = ElementComposition.GetElementVisual(_content); + var compositor = composition!.Compositor; + composition.Opacity = 0; + + var smoothRotationAnimation + = compositor.CreateScalarKeyFrameAnimation(); + smoothRotationAnimation.Target = "RotationAngle"; + smoothRotationAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + smoothRotationAnimation.Duration = TimeSpan.FromMilliseconds(100); + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + + var scaleAnimation + = compositor.CreateVector3KeyFrameAnimation(); + scaleAnimation.Target = "Scale"; + scaleAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + scaleAnimation.Duration = TimeSpan.FromMilliseconds(100); + + var animation = compositor.CreateImplicitAnimationCollection(); + animation["RotationAngle"] = smoothRotationAnimation; + animation["Offset"] = offsetAnimation; + animation["Scale"] = scaleAnimation; + + composition.ImplicitAnimations = animation; + + UpdateContent(); + }; + } - _root.Children.Insert(0, scalingGrid); + _root.Children.Add(_content); _content.VerticalAlignment = Layout.VerticalAlignment.Center; _content.HorizontalAlignment = Layout.HorizontalAlignment.Center; } @@ -294,25 +357,23 @@ namespace Avalonia.Controls } else if (change.Property == BoundsProperty) { - if (_content != null) + switch (PullDirection) { - var parent = _content.Parent as Control; - switch (PullDirection) - { - case PullDirection.TopToBottom: - parent.Margin = new Thickness(0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0); - break; - case PullDirection.BottomToTop: - parent.Margin = new Thickness(0, 0, 0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize)); - break; - case PullDirection.LeftToRight: - parent.Margin = new Thickness(-Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0, 0); - break; - case PullDirection.RightToLeft: - parent.Margin = new Thickness(0, 0, -Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0); - break; - } + case PullDirection.TopToBottom: + RenderTransform = new TranslateTransform(0, -Bounds.Height); + break; + case PullDirection.BottomToTop: + RenderTransform = new TranslateTransform(0, Bounds.Height); + break; + case PullDirection.LeftToRight: + RenderTransform = new TranslateTransform(-Bounds.Width, 0); + break; + case PullDirection.RightToLeft: + RenderTransform = new TranslateTransform(Bounds.Width, 0); + break; } + + UpdateContent(); } } @@ -354,16 +415,15 @@ namespace Avalonia.Controls _interactionRatioSubscription?.Dispose(); _interactionRatioSubscription = null; - if (_refreshInfoProvider != null) + if (RefreshInfoProvider != null) { - _isInteractingSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty) + _isInteractingSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty) .Subscribe(InteractingForRefreshObserver); - _interactionRatioSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty) + _interactionRatioSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty) .Subscribe(InteractionRatioObserver); - var visual = _refreshInfoProvider.Visual; - visual.RenderTransform = _contentTranslateTransform; + var visual = RefreshInfoProvider.Visual; _executingRatio = RefreshInfoProvider.ExecutionRatio; } diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs index 0b0c8c99b2..d8e90c01c0 100644 --- a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs +++ b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Rendering.Composition; using Avalonia.VisualTree; namespace Avalonia.Controls.PullToRefresh @@ -11,9 +12,9 @@ namespace Avalonia.Controls.PullToRefresh private const int InitialOffsetThreshold = 1; private PullDirection _refreshPullDirection; - private ScrollViewer _scrollViewer; - private RefreshInfoProvider _refreshInfoProvider; - private PullGestureRecognizer _pullGestureRecognizer; + private ScrollViewer? _scrollViewer; + private RefreshInfoProvider? _refreshInfoProvider; + private PullGestureRecognizer? _pullGestureRecognizer; private InputElement? _interactionSource; private bool _isVisualizerInteractionSourceAttached; @@ -22,7 +23,7 @@ namespace Avalonia.Controls.PullToRefresh _refreshPullDirection = pullDirection; } - public RefreshInfoProvider AdaptFromTree(IVisual root, Size refreshVIsualizerSize) + public RefreshInfoProvider? AdaptFromTree(IVisual root, Size? refreshVIsualizerSize) { if (root is ScrollViewer scrollViewer) { @@ -44,7 +45,7 @@ namespace Avalonia.Controls.PullToRefresh } } - ScrollViewer AdaptFromTreeRecursiveHelper(IVisual root, int depth) + ScrollViewer? AdaptFromTreeRecursiveHelper(IVisual root, int depth) { if (depth == 0) { @@ -74,7 +75,7 @@ namespace Avalonia.Controls.PullToRefresh return null; } - public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size refreshVIsualizerSize) + public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size? refreshVIsualizerSize) { if (adaptee == null) { @@ -121,7 +122,7 @@ namespace Avalonia.Controls.PullToRefresh } } - _refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, content); + _refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, ElementComposition.GetElementVisual(content)); _pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection); @@ -140,7 +141,7 @@ namespace Avalonia.Controls.PullToRefresh return _refreshInfoProvider; } - private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) + private void ScrollViewer_ScrollChanged(object? sender, ScrollChangedEventArgs e) { if (_isVisualizerInteractionSourceAttached && _refreshInfoProvider != null && _refreshInfoProvider.IsInteractingForRefresh) { @@ -151,9 +152,46 @@ namespace Avalonia.Controls.PullToRefresh } } - private void ScrollViewer_Loaded(object sender, RoutedEventArgs e) + public void SetAnimations(RefreshVisualizer refreshVisualizer) { - var content = _scrollViewer.Content as Visual; + var visualizerComposition = ElementComposition.GetElementVisual(refreshVisualizer); + if (visualizerComposition != null) + { + var compositor = visualizerComposition.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + + var animation = compositor.CreateImplicitAnimationCollection(); + animation["Offset"] = offsetAnimation; + visualizerComposition.ImplicitAnimations = animation; + } + + if(_scrollViewer != null && _scrollViewer.Content is Visual visual) + { + var scollContentComposition = ElementComposition.GetElementVisual(visual); + + if(scollContentComposition != null) + { + var compositor = scollContentComposition.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + + var animation = compositor.CreateImplicitAnimationCollection(); + animation["Offset"] = offsetAnimation; + scollContentComposition.ImplicitAnimations = animation; + } + } + } + + private void ScrollViewer_Loaded(object? sender, RoutedEventArgs? e) + { + var content = _scrollViewer?.Content as Visual; if (content == null) { throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual"); @@ -166,23 +204,26 @@ namespace Avalonia.Controls.PullToRefresh MakeInteractionSource(content.Parent as InputElement); - _scrollViewer.Loaded -= ScrollViewer_Loaded; + if (_scrollViewer != null) + { + _scrollViewer.Loaded -= ScrollViewer_Loaded; + } } - private void MakeInteractionSource(InputElement element) + private void MakeInteractionSource(InputElement? element) { _interactionSource = element; - if (_pullGestureRecognizer != null) + if (_pullGestureRecognizer != null && _refreshInfoProvider != null) { - element.GestureRecognizers.Add(_pullGestureRecognizer); - _interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); - _interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + element?.GestureRecognizers.Add(_pullGestureRecognizer); + _interactionSource?.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource?.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); _isVisualizerInteractionSourceAttached = true; } } - private void ScrollViewer_PointerReleased(object sender, PointerReleasedEventArgs e) + private void ScrollViewer_PointerReleased(object? sender, PointerReleasedEventArgs e) { if (_refreshInfoProvider != null) { @@ -190,9 +231,12 @@ namespace Avalonia.Controls.PullToRefresh } } - private void ScrollViewer_PointerPressed(object sender, PointerPressedEventArgs e) + private void ScrollViewer_PointerPressed(object? sender, PointerPressedEventArgs e) { - _refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold(); + if (_refreshInfoProvider != null) + { + _refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold(); + } } private bool IsWithinOffsetThreashold() @@ -219,9 +263,12 @@ namespace Avalonia.Controls.PullToRefresh private void CleanUpScrollViewer() { - _scrollViewer.PointerPressed -= ScrollViewer_PointerPressed; - _scrollViewer.PointerReleased -= ScrollViewer_PointerReleased; - _scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged; + if (_scrollViewer != null) + { + _scrollViewer.PointerPressed -= ScrollViewer_PointerPressed; + _scrollViewer.PointerReleased -= ScrollViewer_PointerReleased; + _scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged; + } } } } diff --git a/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml index 388ca814f8..3fafabb667 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml @@ -14,37 +14,7 @@ - - - - - + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml index 0b12fe133d..bd7e43530a 100644 --- a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml +++ b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml @@ -18,51 +18,10 @@ MinHeight="80" Background="{TemplateBinding Background}"> - - From 6e1ab5f4578ad7a4edb968f87b203a714c64c0a1 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 18 Nov 2022 12:37:31 +0000 Subject: [PATCH 04/40] remove debug stuff --- src/Avalonia.Base/Input/PullGestureRecognizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Input/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/PullGestureRecognizer.cs index 6939646f13..bbbded44fa 100644 --- a/src/Avalonia.Base/Input/PullGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/PullGestureRecognizer.cs @@ -100,7 +100,7 @@ namespace Avalonia.Input public void PointerPressed(PointerPressedEventArgs e) { - if (_target != null)// && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + if (_target != null && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) { var position = e.GetPosition(_target); From 7d6932bd5ef3fca26b44f47911a162212f4bdfeb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 28 Nov 2022 17:04:14 +0000 Subject: [PATCH 05/40] add failing unit tests. --- .../WindowTests.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 0cdde445d5..f73c3ac215 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -986,7 +986,46 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); } } + + [Fact] + public void IsVisible_Should_Open_Window() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window(); + var raised = false; + + target.Opened += (s, e) => raised = true; + target.IsVisible = true; + Assert.True(raised); + } + } + + [Fact] + public void IsVisible_Should_Close_DialogWindow() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + parent.Show(); + + var target = new Window(); + + var raised = false; + + var task = target.ShowDialog(parent); + + target.Closed += (sender, args) => raised = true; + + target.IsVisible = false; + + Assert.True(raised); + + Assert.False(task.Result); + } + } + protected virtual void Show(Window window) { window.Show(); From a7a3df912a4984273fd20788de9afe13022d4d39 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 29 Nov 2022 09:46:27 +0000 Subject: [PATCH 06/40] implement isvisible controlling show / hide / close (for dialogs) --- src/Avalonia.Controls/Window.cs | 50 +++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 559d674c02..672099c3bb 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -72,6 +72,9 @@ namespace Avalonia.Controls private bool _isExtendedIntoWindowDecorations; private Thickness _windowDecorationMargin; private Thickness _offScreenMargin; + private bool _shown; + private bool _showingAsDialog; + private bool _inShowHideMethods; /// /// Defines the property. @@ -506,8 +509,14 @@ namespace Avalonia.Controls } Owner = null; + _showingAsDialog = false; + _shown = false; + _inShowHideMethods = true; PlatformImpl?.Dispose(); + + _inShowHideMethods = false; + } private bool ShouldCancelClose(CancelEventArgs? args = null) @@ -563,7 +572,7 @@ namespace Avalonia.Controls /// public override void Hide() { - if (!IsVisible) + if (!_shown) { return; } @@ -585,7 +594,10 @@ namespace Avalonia.Controls Owner = null; PlatformImpl?.Hide(); + _shown = false; + _inShowHideMethods = true; IsVisible = false; + _inShowHideMethods = false; } /// @@ -639,7 +651,7 @@ namespace Avalonia.Controls } } - if (IsVisible) + if (_shown) { return; } @@ -648,7 +660,10 @@ namespace Avalonia.Controls EnsureInitialized(); ApplyStyling(); + _inShowHideMethods = true; + _shown = true; IsVisible = true; + _inShowHideMethods = false; var initialSize = new Size( double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, @@ -728,7 +743,11 @@ namespace Avalonia.Controls EnsureInitialized(); ApplyStyling(); + _shown = true; + _showingAsDialog = true; + _inShowHideMethods = true; IsVisible = true; + _inShowHideMethods = false; var initialSize = new Size( double.IsNaN(Width) ? ClientSize.Width : Width, @@ -999,6 +1018,33 @@ namespace Avalonia.Controls PlatformImpl?.SetSystemDecorations(typedNewValue); } + + if (change.Property == IsVisibleProperty) + { + if (!_inShowHideMethods) + { + var isVisible = change.GetNewValue(); + + if (_shown != isVisible) + { + if (!_shown) + { + ShowCore(null); + } + else + { + if (_showingAsDialog) + { + Close(false); + } + else + { + Hide(); + } + } + } + } + } } protected override AutomationPeer OnCreateAutomationPeer() From f4804551316b4141367f3ef3fa38c50d9a702fc0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Nov 2022 12:39:24 +0100 Subject: [PATCH 07/40] Added failing test for #9565. And another (passing) test for checking that window order is correct when a child window is shown in fullscreen. --- samples/IntegrationTestApp/MainWindow.axaml | 7 +- .../IntegrationTestApp/MainWindow.axaml.cs | 3 + .../WindowTests_MacOS.cs | 74 +++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 038ced4e5c..801ff765c8 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -17,11 +17,15 @@ - + + + WindowState: + + @@ -136,6 +140,7 @@ + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index c1acc7ca88..841947673a 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Avalonia; @@ -178,6 +179,8 @@ namespace IntegrationTestApp ShowWindow(); if (source?.Name == "SendToBack") SendToBack(); + if (source?.Name == "EnterFullscreen") + WindowState = WindowState.FullScreen; if (source?.Name == "ExitFullscreen") WindowState = WindowState.Normal; if (source?.Name == "RestoreAll") diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 05ed0616a8..8f4417a451 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using Avalonia.Controls; +using Avalonia.Utilities; using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Interactions; @@ -114,6 +115,79 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal(1, secondaryWindowIndex); } } + + [PlatformFact(TestPlatforms.MacOS)] + public void WindowOrder_Owned_Dialog_Stays_InFront_Of_FullScreen_Parent() + { + var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + + // Enter fullscreen + mainWindow.FindElementByAccessibilityId("EnterFullscreen").Click(); + + // Wait for fullscreen transition. + Thread.Sleep(1000); + + // Make sure we entered fullscreen. + var windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + Assert.Equal("FullScreen", windowState.Text); + + // Open child window. + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.Manual)) + { + mainWindow.SendClick(); + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); + Assert.Equal(1, secondaryWindowIndex); + } + + // Exit fullscreen by menu shortcut Command+R + mainWindow.FindElementByAccessibilityId("ExitFullscreen").Click(); + + // Wait for restore transition. + Thread.Sleep(1000); + + // Make sure we exited fullscreen. + mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + Assert.Equal("Normal", windowState.Text); + } + + [PlatformFact(TestPlatforms.MacOS)] + public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked() + { + // Issue #9565 + var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + AppiumWebElement windowState; + + // Open child window. + using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.Manual)) + { + // Enter fullscreen + mainWindow.FindElementByAccessibilityId("EnterFullscreen").Click(); + + // Wait for fullscreen transition. + Thread.Sleep(1000); + + // Make sure we entered fullscreen. + mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + Assert.Equal("FullScreen", windowState.Text); + + // Click on main window + mainWindow.Click(); + + // Failed here due to #9565: main window is no longer visible as the main space is now shown instead + // of the fullscreen space. + mainWindow.FindElementByAccessibilityId("ExitFullscreen").Click(); + + // Wait for restore transition. + Thread.Sleep(1000); + } + + // Make sure we exited fullscreen. + mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + Assert.Equal("Normal", windowState.Text); + } [PlatformFact(TestPlatforms.MacOS)] public void WindowOrder_NonOwned_Window_Does_Not_Stay_InFront_Of_Parent() From d00f19d55671373ea0329e35e4a52afb3815364b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Nov 2022 12:40:02 +0100 Subject: [PATCH 08/40] Fix #9565 - Only bring window to front if it's on the currently active space - Ensure correct order of child windows after fullscreen transition --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 2443965957..b6dacb6ce4 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -137,7 +137,11 @@ void WindowImpl::BringToFront() for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) { - (*iterator)->BringToFront(); + auto window = (*iterator)->Window; + + // #9565: Only bring window to front if it's on the currently active space + if ([window isOnActiveSpace]) + (*iterator)->BringToFront(); } } } @@ -161,6 +165,9 @@ void WindowImpl::StartStateTransition() { void WindowImpl::EndStateTransition() { _transitioningWindowState = false; + + // Ensure correct order of child windows after fullscreen transition. + BringToFront(); } SystemDecorations WindowImpl::Decorations() { From fdf76c5765ceb8f2c4191aa1257c2eb6cea3bccd Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Wed, 30 Nov 2022 14:50:27 +0200 Subject: [PATCH 09/40] Replace IVisual with Visual --- nukebuild/Numerge | 2 +- src/Avalonia.Base/Animation/CrossFade.cs | 2 +- src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs | 2 +- src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs | 2 +- src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs | 2 +- src/Avalonia.Base/Media/MatrixTransform.cs | 2 +- src/Avalonia.Base/Media/RotateTransform.cs | 2 +- src/Avalonia.Base/Media/ScaleTransform.cs | 2 +- src/Avalonia.Base/Media/SkewTransform.cs | 2 +- src/Avalonia.Base/Media/Transform.cs | 2 +- src/Avalonia.Base/Media/TranslateTransform.cs | 2 +- src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/nukebuild/Numerge b/nukebuild/Numerge index aef10ae67d..fb92f917cd 160000 --- a/nukebuild/Numerge +++ b/nukebuild/Numerge @@ -1 +1 @@ -Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5 +Subproject commit fb92f917cd2d3aaec0d2294635d922184ff1e0fc diff --git a/src/Avalonia.Base/Animation/CrossFade.cs b/src/Avalonia.Base/Animation/CrossFade.cs index 608a0880ec..a229bc7ce6 100644 --- a/src/Avalonia.Base/Animation/CrossFade.cs +++ b/src/Avalonia.Base/Animation/CrossFade.cs @@ -10,7 +10,7 @@ using Avalonia.VisualTree; namespace Avalonia.Animation { /// - /// Defines a cross-fade animation between two s. + /// Defines a cross-fade animation between two s. /// public class CrossFade : IPageTransition { diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index c4508c3f5c..d56711ad68 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -7,7 +7,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media.Imaging { /// - /// A bitmap that holds the rendering of a . + /// A bitmap that holds the rendering of a . /// public class RenderTargetBitmap : Bitmap, IDisposable, IRenderTarget { diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs b/src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs index d5ff2b8317..4478504eca 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableTransform.cs @@ -3,7 +3,7 @@ namespace Avalonia.Media.Immutable { /// - /// Represents a transform on an . + /// Represents a transform on an . /// public class ImmutableTransform : ITransform { diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs index 1e0133c9b7..9b443391c5 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media.Immutable { /// - /// Paints an area with an . + /// Paints an area with an . /// internal class ImmutableVisualBrush : ImmutableTileBrush, IVisualBrush { diff --git a/src/Avalonia.Base/Media/MatrixTransform.cs b/src/Avalonia.Base/Media/MatrixTransform.cs index 4e60e1e290..c61acb730c 100644 --- a/src/Avalonia.Base/Media/MatrixTransform.cs +++ b/src/Avalonia.Base/Media/MatrixTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Transforms an according to a . + /// Transforms an according to a . /// public class MatrixTransform : Transform { diff --git a/src/Avalonia.Base/Media/RotateTransform.cs b/src/Avalonia.Base/Media/RotateTransform.cs index 126bb7c274..3bd409149c 100644 --- a/src/Avalonia.Base/Media/RotateTransform.cs +++ b/src/Avalonia.Base/Media/RotateTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Rotates an . + /// Rotates an . /// public class RotateTransform : Transform { diff --git a/src/Avalonia.Base/Media/ScaleTransform.cs b/src/Avalonia.Base/Media/ScaleTransform.cs index 259b23cbd2..d4c1a7f993 100644 --- a/src/Avalonia.Base/Media/ScaleTransform.cs +++ b/src/Avalonia.Base/Media/ScaleTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Scale an . + /// Scale an . /// public class ScaleTransform : Transform { diff --git a/src/Avalonia.Base/Media/SkewTransform.cs b/src/Avalonia.Base/Media/SkewTransform.cs index a96710e331..066f5371c3 100644 --- a/src/Avalonia.Base/Media/SkewTransform.cs +++ b/src/Avalonia.Base/Media/SkewTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Skews an . + /// Skews an . /// public class SkewTransform : Transform { diff --git a/src/Avalonia.Base/Media/Transform.cs b/src/Avalonia.Base/Media/Transform.cs index 023a8b9cdd..85393ab189 100644 --- a/src/Avalonia.Base/Media/Transform.cs +++ b/src/Avalonia.Base/Media/Transform.cs @@ -7,7 +7,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Represents a transform on an . + /// Represents a transform on an . /// public abstract class Transform : Animatable, IMutableTransform { diff --git a/src/Avalonia.Base/Media/TranslateTransform.cs b/src/Avalonia.Base/Media/TranslateTransform.cs index d6d6809f3d..0f910f3600 100644 --- a/src/Avalonia.Base/Media/TranslateTransform.cs +++ b/src/Avalonia.Base/Media/TranslateTransform.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Media { /// - /// Translates (moves) an . + /// Translates (moves) an . /// public class TranslateTransform : Transform { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs index 4eec214f4f..a991f2f657 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs @@ -10,7 +10,7 @@ using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph { /// - /// A node in the low-level scene graph representing an . + /// A node in the low-level scene graph representing an . /// internal class VisualNode : IVisualNode { From 8c019e669194784a29243a4922b6beb98dbf99e2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 30 Nov 2022 14:15:25 +0000 Subject: [PATCH 10/40] simpler fix. --- src/Avalonia.Controls/Window.cs | 47 ++------------------------------- 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 672099c3bb..280853967a 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -72,9 +72,6 @@ namespace Avalonia.Controls private bool _isExtendedIntoWindowDecorations; private Thickness _windowDecorationMargin; private Thickness _offScreenMargin; - private bool _shown; - private bool _showingAsDialog; - private bool _inShowHideMethods; /// /// Defines the property. @@ -179,6 +176,7 @@ namespace Avalonia.Controls private object? _dialogResult; private readonly Size _maxPlatformClientSize; private WindowStartupLocation _windowStartupLocation; + private bool _shown; /// /// Initializes static members of the class. @@ -509,14 +507,8 @@ namespace Avalonia.Controls } Owner = null; - _showingAsDialog = false; - _shown = false; - _inShowHideMethods = true; PlatformImpl?.Dispose(); - - _inShowHideMethods = false; - } private bool ShouldCancelClose(CancelEventArgs? args = null) @@ -594,10 +586,8 @@ namespace Avalonia.Controls Owner = null; PlatformImpl?.Hide(); - _shown = false; - _inShowHideMethods = true; IsVisible = false; - _inShowHideMethods = false; + _shown = false; } /// @@ -660,10 +650,8 @@ namespace Avalonia.Controls EnsureInitialized(); ApplyStyling(); - _inShowHideMethods = true; _shown = true; IsVisible = true; - _inShowHideMethods = false; var initialSize = new Size( double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, @@ -743,11 +731,7 @@ namespace Avalonia.Controls EnsureInitialized(); ApplyStyling(); - _shown = true; - _showingAsDialog = true; - _inShowHideMethods = true; IsVisible = true; - _inShowHideMethods = false; var initialSize = new Size( double.IsNaN(Width) ? ClientSize.Width : Width, @@ -1018,33 +1002,6 @@ namespace Avalonia.Controls PlatformImpl?.SetSystemDecorations(typedNewValue); } - - if (change.Property == IsVisibleProperty) - { - if (!_inShowHideMethods) - { - var isVisible = change.GetNewValue(); - - if (_shown != isVisible) - { - if (!_shown) - { - ShowCore(null); - } - else - { - if (_showingAsDialog) - { - Close(false); - } - else - { - Hide(); - } - } - } - } - } } protected override AutomationPeer OnCreateAutomationPeer() From fb821c7899ca6ca1e900526df25d74326dddbb23 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 30 Nov 2022 14:26:34 +0000 Subject: [PATCH 11/40] ensure that isvisible = false can close dialogs. --- src/Avalonia.Controls/Window.cs | 230 +++++++++++++++++----------- src/Avalonia.Controls/WindowBase.cs | 18 +-- 2 files changed, 149 insertions(+), 99 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 280853967a..aa0ce50bad 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -177,6 +177,7 @@ namespace Avalonia.Controls private readonly Size _maxPlatformClientSize; private WindowStartupLocation _windowStartupLocation; private bool _shown; + private bool _showingAsDialog; /// /// Initializes static members of the class. @@ -509,6 +510,8 @@ namespace Avalonia.Controls Owner = null; PlatformImpl?.Dispose(); + + _showingAsDialog = false; } private bool ShouldCancelClose(CancelEventArgs? args = null) @@ -601,6 +604,33 @@ namespace Avalonia.Controls ShowCore(null); } + protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!IgnoreVisibilityChange) + { + var isVisible = e.GetNewValue(); + + if (_shown != isVisible) + { + if(!_shown) + { + Show(); + } + else + { + if (_showingAsDialog) + { + Close(false); + } + else + { + Hide(); + } + } + } + } + } + /// /// Shows the window as a child of . /// @@ -620,63 +650,72 @@ namespace Avalonia.Controls private void ShowCore(Window? parent) { - if (PlatformImpl == null) - { - throw new InvalidOperationException("Cannot re-show a closed window."); - } - - if (parent != null) + try { - if (parent.PlatformImpl == null) + IgnoreVisibilityChange = true; + + if (PlatformImpl == null) { - throw new InvalidOperationException("Cannot show a window with a closed parent."); + throw new InvalidOperationException("Cannot re-show a closed window."); } - else if (parent == this) + + if (parent != null) { - throw new InvalidOperationException("A Window cannot be its own parent."); + if (parent.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot show a window with a closed parent."); + } + else if (parent == this) + { + throw new InvalidOperationException("A Window cannot be its own parent."); + } + else if (!parent.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible parent."); + } } - else if (!parent.IsVisible) + + if (_shown) { - throw new InvalidOperationException("Cannot show window with non-visible parent."); + return; } - } - if (_shown) - { - return; - } - - RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); + RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); - EnsureInitialized(); - ApplyStyling(); - _shown = true; - IsVisible = true; + EnsureInitialized(); + ApplyStyling(); + _shown = true; + IsVisible = true; - var initialSize = new Size( - double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, - double.IsNaN(Height) ? Math.Max(MinHeight, ClientSize.Height) : Height); + var initialSize = new Size( + double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, + double.IsNaN(Height) ? Math.Max(MinHeight, ClientSize.Height) : Height); - if (initialSize != ClientSize) - { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); - } + if (initialSize != ClientSize) + { + PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + } - LayoutManager.ExecuteInitialLayoutPass(); + LayoutManager.ExecuteInitialLayoutPass(); - if (PlatformImpl != null && parent?.PlatformImpl is not null) - { - PlatformImpl.SetParent(parent.PlatformImpl); - } + if (PlatformImpl != null && parent?.PlatformImpl is not null) + { + PlatformImpl.SetParent(parent.PlatformImpl); + } - Owner = parent; - parent?.AddChild(this, false); + Owner = parent; + parent?.AddChild(this, false); - SetWindowStartupLocation(parent?.PlatformImpl); + SetWindowStartupLocation(parent?.PlatformImpl); - PlatformImpl?.Show(ShowActivated, false); - Renderer?.Start(); - OnOpened(EventArgs.Empty); + PlatformImpl?.Show(ShowActivated, false); + Renderer?.Start(); + OnOpened(EventArgs.Empty); + } + finally + { + IgnoreVisibilityChange = false; + } } /// @@ -706,68 +745,79 @@ namespace Avalonia.Controls /// public Task ShowDialog(Window owner) { - if (owner == null) - { - throw new ArgumentNullException(nameof(owner)); - } - else if (owner.PlatformImpl == null) - { - throw new InvalidOperationException("Cannot show a window with a closed owner."); - } - else if (owner == this) - { - throw new InvalidOperationException("A Window cannot be its own owner."); - } - else if (IsVisible) - { - throw new InvalidOperationException("The window is already being shown."); - } - else if (!owner.IsVisible) + try { - throw new InvalidOperationException("Cannot show window with non-visible parent."); - } + IgnoreVisibilityChange = true; + + if (owner == null) + { + throw new ArgumentNullException(nameof(owner)); + } + else if (owner.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot show a window with a closed owner."); + } + else if (owner == this) + { + throw new InvalidOperationException("A Window cannot be its own owner."); + } + else if (IsVisible) + { + throw new InvalidOperationException("The window is already being shown."); + } + else if (!owner.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible parent."); + } - RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); + RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); - EnsureInitialized(); - ApplyStyling(); - IsVisible = true; + EnsureInitialized(); + ApplyStyling(); + _shown = true; + _showingAsDialog = true; + IsVisible = true; - var initialSize = new Size( - double.IsNaN(Width) ? ClientSize.Width : Width, - double.IsNaN(Height) ? ClientSize.Height : Height); + var initialSize = new Size( + double.IsNaN(Width) ? ClientSize.Width : Width, + double.IsNaN(Height) ? ClientSize.Height : Height); - if (initialSize != ClientSize) - { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); - } + if (initialSize != ClientSize) + { + PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + } - LayoutManager.ExecuteInitialLayoutPass(); + LayoutManager.ExecuteInitialLayoutPass(); - var result = new TaskCompletionSource(); + var result = new TaskCompletionSource(); - PlatformImpl?.SetParent(owner.PlatformImpl); - Owner = owner; - owner.AddChild(this, true); + PlatformImpl?.SetParent(owner.PlatformImpl); + Owner = owner; + owner.AddChild(this, true); - SetWindowStartupLocation(owner.PlatformImpl); + SetWindowStartupLocation(owner.PlatformImpl); - PlatformImpl?.Show(ShowActivated, true); + PlatformImpl?.Show(ShowActivated, true); - Renderer?.Start(); + Renderer?.Start(); - Observable.FromEventPattern( - x => Closed += x, - x => Closed -= x) - .Take(1) - .Subscribe(_ => - { - owner.Activate(); - result.SetResult((TResult)(_dialogResult ?? default(TResult)!)); - }); + Observable.FromEventPattern( + x => Closed += x, + x => Closed -= x) + .Take(1) + .Subscribe(_ => + { + owner.Activate(); + result.SetResult((TResult)(_dialogResult ?? default(TResult)!)); + }); - OnOpened(EventArgs.Empty); - return result.Task; + OnOpened(EventArgs.Empty); + return result.Task; + } + finally + { + IgnoreVisibilityChange = false; + } } private void UpdateEnabled() diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 8f1b2198ad..46653c8203 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls private bool _hasExecutedInitialLayoutPass; private bool _isActive; - private bool _ignoreVisibilityChange; + protected bool IgnoreVisibilityChange { get; set; } private WindowBase? _owner; static WindowBase() @@ -125,7 +125,7 @@ namespace Avalonia.Controls /// public virtual void Hide() { - _ignoreVisibilityChange = true; + IgnoreVisibilityChange = true; try { @@ -135,7 +135,7 @@ namespace Avalonia.Controls } finally { - _ignoreVisibilityChange = false; + IgnoreVisibilityChange = false; } } @@ -144,7 +144,7 @@ namespace Avalonia.Controls /// public virtual void Show() { - _ignoreVisibilityChange = true; + IgnoreVisibilityChange = true; try { @@ -163,7 +163,7 @@ namespace Avalonia.Controls } finally { - _ignoreVisibilityChange = false; + IgnoreVisibilityChange = false; } } @@ -202,7 +202,7 @@ namespace Avalonia.Controls protected override void HandleClosed() { - _ignoreVisibilityChange = true; + IgnoreVisibilityChange = true; try { @@ -217,7 +217,7 @@ namespace Avalonia.Controls } finally { - _ignoreVisibilityChange = false; + IgnoreVisibilityChange = false; } } @@ -318,9 +318,9 @@ namespace Avalonia.Controls Deactivated?.Invoke(this, EventArgs.Empty); } - private void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) + protected virtual void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) { - if (!_ignoreVisibilityChange) + if (!IgnoreVisibilityChange) { if ((bool)e.NewValue!) { From 4a490694a24f7e0cd2eff83cd7832fb2f18655f9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 30 Nov 2022 20:08:42 +0000 Subject: [PATCH 12/40] fix unit test. --- src/Avalonia.Controls/Window.cs | 43 ++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index aa0ce50bad..1ff08f0169 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -567,30 +567,39 @@ namespace Avalonia.Controls /// public override void Hide() { - if (!_shown) + try { - return; - } + IgnoreVisibilityChange = true; + + if (!_shown) + { + return; + } - Renderer?.Stop(); + Renderer?.Stop(); - if (Owner is Window owner) - { - owner.RemoveChild(this); - } + if (Owner is Window owner) + { + owner.RemoveChild(this); + } - if (_children.Count > 0) - { - foreach (var child in _children.ToArray()) + if (_children.Count > 0) { - child.child.Hide(); + foreach (var child in _children.ToArray()) + { + child.child.Hide(); + } } - } - Owner = null; - PlatformImpl?.Hide(); - IsVisible = false; - _shown = false; + Owner = null; + PlatformImpl?.Hide(); + IsVisible = false; + _shown = false; + } + finally + { + IgnoreVisibilityChange = false; + } } /// From 1af7eb6f3a93b5ed3b109abd9041409b041ebd9e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Dec 2022 17:17:07 +0000 Subject: [PATCH 13/40] fix window showdialog, and show raising exceptions when incorrect behavior is used. --- src/Avalonia.Controls/Window.cs | 99 ++++++++++--------- .../WindowTests.cs | 8 +- 2 files changed, 56 insertions(+), 51 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 1ff08f0169..6b936e495c 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -641,47 +641,64 @@ namespace Avalonia.Controls } /// - /// Shows the window as a child of . + /// Shows the window as a child of . /// - /// Window that will be a parent of the shown window. + /// Window that will be the owner of the shown window. /// /// The window has already been closed. /// - public void Show(Window parent) + public void Show(Window owner) { - if (parent is null) + if (owner is null) { - throw new ArgumentNullException(nameof(parent), "Showing a child window requires valid parent."); + throw new ArgumentNullException(nameof(owner), "Showing a child window requires valid parent."); } - ShowCore(parent); + ShowCore(owner); } - private void ShowCore(Window? parent) + private void EnsureStateBeforeShow() + { + if (PlatformImpl == null) + { + throw new InvalidOperationException("Cannot re-show a closed window."); + } + + if (_shown) + { + throw new InvalidOperationException("The window is already being shown."); + } + } + + private void EnsureParentStateBeforeShow(Window owner) + { + if (owner.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot show a window with a closed owner."); + } + + if (owner == this) + { + throw new InvalidOperationException("A Window cannot be its own owner."); + } + + if (!owner.IsVisible) + { + throw new InvalidOperationException("Cannot show window with non-visible owner."); + } + } + + private void ShowCore(Window? owner) { try { IgnoreVisibilityChange = true; - - if (PlatformImpl == null) - { - throw new InvalidOperationException("Cannot re-show a closed window."); - } - if (parent != null) + EnsureStateBeforeShow(); + + if (owner != null) { - if (parent.PlatformImpl == null) - { - throw new InvalidOperationException("Cannot show a window with a closed parent."); - } - else if (parent == this) - { - throw new InvalidOperationException("A Window cannot be its own parent."); - } - else if (!parent.IsVisible) - { - throw new InvalidOperationException("Cannot show window with non-visible parent."); - } + EnsureParentStateBeforeShow(owner); } if (_shown) @@ -707,15 +724,15 @@ namespace Avalonia.Controls LayoutManager.ExecuteInitialLayoutPass(); - if (PlatformImpl != null && parent?.PlatformImpl is not null) + if (PlatformImpl != null && owner?.PlatformImpl is not null) { - PlatformImpl.SetParent(parent.PlatformImpl); + PlatformImpl.SetParent(owner.PlatformImpl); } - Owner = parent; - parent?.AddChild(this, false); + Owner = owner; + owner?.AddChild(this, false); - SetWindowStartupLocation(parent?.PlatformImpl); + SetWindowStartupLocation(owner?.PlatformImpl); PlatformImpl?.Show(ShowActivated, false); Renderer?.Start(); @@ -758,26 +775,14 @@ namespace Avalonia.Controls { IgnoreVisibilityChange = true; + EnsureStateBeforeShow(); + if (owner == null) { throw new ArgumentNullException(nameof(owner)); } - else if (owner.PlatformImpl == null) - { - throw new InvalidOperationException("Cannot show a window with a closed owner."); - } - else if (owner == this) - { - throw new InvalidOperationException("A Window cannot be its own owner."); - } - else if (IsVisible) - { - throw new InvalidOperationException("The window is already being shown."); - } - else if (!owner.IsVisible) - { - throw new InvalidOperationException("Cannot show window with non-visible parent."); - } + + EnsureParentStateBeforeShow(owner); RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); @@ -800,7 +805,7 @@ namespace Avalonia.Controls var result = new TaskCompletionSource(); - PlatformImpl?.SetParent(owner.PlatformImpl); + PlatformImpl?.SetParent(owner.PlatformImpl!); Owner = owner; owner.AddChild(this, true); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index f73c3ac215..e544ca62bb 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -403,7 +403,7 @@ namespace Avalonia.Controls.UnitTests parent.Close(); var ex = Assert.Throws(() => target.Show(parent)); - Assert.Equal("Cannot show a window with a closed parent.", ex.Message); + Assert.Equal("Cannot show a window with a closed owner.", ex.Message); } } @@ -431,7 +431,7 @@ namespace Avalonia.Controls.UnitTests var target = new Window(); var ex = Assert.Throws(() => target.Show(parent)); - Assert.Equal("Cannot show window with non-visible parent.", ex.Message); + Assert.Equal("Cannot show window with non-visible owner.", ex.Message); } } @@ -444,7 +444,7 @@ namespace Avalonia.Controls.UnitTests var target = new Window(); var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); - Assert.Equal("Cannot show window with non-visible parent.", ex.Message); + Assert.Equal("Cannot show window with non-visible owner.", ex.Message); } } @@ -456,7 +456,7 @@ namespace Avalonia.Controls.UnitTests var target = new Window(); var ex = Assert.Throws(() => target.Show(target)); - Assert.Equal("A Window cannot be its own parent.", ex.Message); + Assert.Equal("A Window cannot be its own owner.", ex.Message); } } From e7039842f5c38900a4dd3253207b99b95465c010 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Dec 2022 17:34:07 +0000 Subject: [PATCH 14/40] Show can be called multiple times but ignored. --- src/Avalonia.Controls/Window.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 6b936e495c..729bbaa4b3 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -663,11 +663,6 @@ namespace Avalonia.Controls { throw new InvalidOperationException("Cannot re-show a closed window."); } - - if (_shown) - { - throw new InvalidOperationException("The window is already being shown."); - } } private void EnsureParentStateBeforeShow(Window owner) @@ -783,6 +778,11 @@ namespace Avalonia.Controls } EnsureParentStateBeforeShow(owner); + + if (_shown) + { + throw new InvalidOperationException("The window is already being shown."); + } RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); From 915aff2eeace37ad57b16493c348159e64a2f715 Mon Sep 17 00:00:00 2001 From: zhouzj Date: Mon, 5 Dec 2022 14:34:33 +0800 Subject: [PATCH 15/40] Prevent showing trayMenu if the menu is empty. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 8d565d7fef..ddc42b3248 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -19,7 +19,7 @@ namespace Avalonia.Win32 public class TrayIconImpl : ITrayIconImpl { private static readonly IntPtr s_emptyIcon = new System.Drawing.Bitmap(32, 32).GetHicon(); - + private readonly int _uniqueId; private static int s_nextUniqueId; private bool _iconAdded; @@ -137,6 +137,12 @@ namespace Avalonia.Win32 private void OnRightClicked() { + var menuItems = _exporter.GetMenu(); + if (null == menuItems || menuItems.Count == 0) + { + return; + } + var _trayMenu = new TrayPopupRoot() { SystemDecorations = SystemDecorations.None, @@ -145,7 +151,7 @@ namespace Avalonia.Win32 TransparencyLevelHint = WindowTransparencyLevel.Transparent, Content = new TrayIconMenuFlyoutPresenter() { - Items = _exporter.GetMenu() + Items = menuItems } }; From 19043beda148892ee5d60d5786c9c811c0170309 Mon Sep 17 00:00:00 2001 From: zhouzj Date: Mon, 5 Dec 2022 14:40:10 +0800 Subject: [PATCH 16/40] fix typo --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index ddc42b3248..ab70d77a09 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -19,7 +19,6 @@ namespace Avalonia.Win32 public class TrayIconImpl : ITrayIconImpl { private static readonly IntPtr s_emptyIcon = new System.Drawing.Bitmap(32, 32).GetHicon(); - private readonly int _uniqueId; private static int s_nextUniqueId; private bool _iconAdded; From 4fae1b6e7abf8ab1ccd7090110e8cc2055d4edb4 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 5 Dec 2022 10:15:05 +0200 Subject: [PATCH 17/40] Move FlewDirection to Visual and fix tests --- src/Avalonia.Base/Visual.cs | 90 +++++++++++++++++++ src/Avalonia.Controls/ComboBox.cs | 5 +- src/Avalonia.Controls/Control.cs | 85 ------------------ .../FlowDirectionTests.cs | 27 +++--- 4 files changed, 108 insertions(+), 99 deletions(-) diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 7694119589..e3dc4fbb75 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -89,6 +89,14 @@ namespace Avalonia public static readonly StyledProperty RenderTransformOriginProperty = AvaloniaProperty.Register(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center); + /// + /// Defines the property. + /// + public static readonly AttachedProperty FlowDirectionProperty = + AvaloniaProperty.RegisterAttached( + nameof(FlowDirection), + inherits: true); + /// /// Defines the property. /// @@ -263,6 +271,15 @@ namespace Avalonia set { SetValue(RenderTransformOriginProperty, value); } } + /// + /// Gets or sets the text flow direction. + /// + public FlowDirection FlowDirection + { + get => GetValue(FlowDirectionProperty); + set => SetValue(FlowDirectionProperty, value); + } + /// /// Gets or sets the Z index of the control. /// @@ -306,6 +323,36 @@ namespace Avalonia /// internal Visual? VisualParent => _visualParent; + /// + /// Gets a value indicating whether control bypass FlowDirecton policies. + /// + /// + /// Related to FlowDirection system and returns false as default, so if + /// is RTL then control will get a mirror presentation. + /// For controls that want to avoid this behavior, override this property and return true. + /// + protected virtual bool BypassFlowDirectionPolicies => false; + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The flow direction. + public static FlowDirection GetFlowDirection(Visual visual) + { + return visual.GetValue(FlowDirectionProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFlowDirection(Visual visual, FlowDirection value) + { + visual.SetValue(FlowDirectionProperty, value); + } + /// /// Invalidates the visual and queues a repaint. /// @@ -387,6 +434,22 @@ namespace Avalonia } } + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == FlowDirectionProperty) + { + InvalidateMirrorTransform(); + + foreach (var child in VisualChildren) + { + child.InvalidateMirrorTransform(); + } + } + } + protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { base.LogicalChildrenCollectionChanged(sender, e); @@ -682,5 +745,32 @@ namespace Avalonia visual.SetVisualParent(parent); } } + + /// + /// Computes the value according to the + /// and + /// + public virtual void InvalidateMirrorTransform() + { + var flowDirection = this.FlowDirection; + var parentFlowDirection = FlowDirection.LeftToRight; + + bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies; + bool parentBypassFlowDirectionPolicies = false; + + var parent = VisualParent; + if (parent != null) + { + parentFlowDirection = parent.FlowDirection; + parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies; + } + + bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies; + bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies; + + bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored; + + HasMirrorTransform = shouldApplyMirrorTransform; + } } } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 2b407cc42a..f02df2e9c1 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -454,10 +454,9 @@ namespace Avalonia.Controls { if (SelectionBoxItem is Rectangle rectangle) { - if ((rectangle.Fill as VisualBrush)?.Visual is Control content) + if ((rectangle.Fill as VisualBrush)?.Visual is Visual content) { - var flowDirection = (((Visual)content!).VisualParent as Control)?.FlowDirection ?? - FlowDirection.LeftToRight; + var flowDirection = content.VisualParent?.FlowDirection ?? FlowDirection.LeftToRight; rectangle.FlowDirection = flowDirection; } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 063e6ae7c8..88c9823952 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -91,13 +91,6 @@ namespace Avalonia.Controls RoutedEvent.Register( nameof(SizeChanged), RoutingStrategies.Direct); - /// - /// Defines the property. - /// - public static readonly AttachedProperty FlowDirectionProperty = - AvaloniaProperty.RegisterAttached( - nameof(FlowDirection), - inherits: true); // Note the following: // _loadedQueue : @@ -170,15 +163,6 @@ namespace Avalonia.Controls get => GetValue(TagProperty); set => SetValue(TagProperty, value); } - - /// - /// Gets or sets the text flow direction. - /// - public FlowDirection FlowDirection - { - get => GetValue(FlowDirectionProperty); - set => SetValue(FlowDirectionProperty, value); - } /// /// Occurs when the user has completed a context input gesture, such as a right-click. @@ -229,39 +213,9 @@ namespace Avalonia.Controls public new Control? Parent => (Control?)base.Parent; - /// - /// Gets the value of the attached on a control. - /// - /// The control. - /// The flow direction. - public static FlowDirection GetFlowDirection(Control control) - { - return control.GetValue(FlowDirectionProperty); - } - - /// - /// Sets the value of the attached on a control. - /// - /// The control. - /// The property value to set. - public static void SetFlowDirection(Control control, FlowDirection value) - { - control.SetValue(FlowDirectionProperty, value); - } - /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; - /// - /// Gets a value indicating whether control bypass FlowDirecton policies. - /// - /// - /// Related to FlowDirection system and returns false as default, so if - /// is RTL then control will get a mirror presentation. - /// For controls that want to avoid this behavior, override this property and return true. - /// - protected virtual bool BypassFlowDirectionPolicies => false; - /// void ISetterValue.Initialize(ISetter setter) { @@ -571,45 +525,6 @@ namespace Avalonia.Controls RaiseEvent(sizeChangedEventArgs); } } - else if (change.Property == FlowDirectionProperty) - { - InvalidateMirrorTransform(); - - foreach (var visual in VisualChildren) - { - if (visual is Control child) - { - child.InvalidateMirrorTransform(); - } - } - } - } - - /// - /// Computes the value according to the - /// and - /// - public virtual void InvalidateMirrorTransform() - { - var flowDirection = this.FlowDirection; - var parentFlowDirection = FlowDirection.LeftToRight; - - bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies; - bool parentBypassFlowDirectionPolicies = false; - - var parent = this.VisualParent as Control; - if (parent != null) - { - parentFlowDirection = parent.FlowDirection; - parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies; - } - - bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies; - bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies; - - bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored; - - HasMirrorTransform = shouldApplyMirrorTransform; } } } diff --git a/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs b/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs index 6c43103ecb..f790ed7412 100644 --- a/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs +++ b/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs @@ -8,7 +8,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void HasMirrorTransform_Should_Be_True() { - var target = new Control + var target = new Visual { FlowDirection = FlowDirection.RightToLeft, }; @@ -19,31 +19,36 @@ namespace Avalonia.Controls.UnitTests [Fact] public void HasMirrorTransform_Of_LTR_Children_Should_Be_True_For_RTL_Parent() { - Control child; - var target = new Decorator + var child = new Visual() + { + FlowDirection = FlowDirection.LeftToRight, + }; + + var target = new Visual { FlowDirection = FlowDirection.RightToLeft, - Child = child = new Control() }; + target.VisualChildren.Add(child); - child.FlowDirection = FlowDirection.LeftToRight; + child.InvalidateMirrorTransform(); Assert.True(target.HasMirrorTransform); Assert.True(child.HasMirrorTransform); } [Fact] - public void HasMirrorTransform_Of_Children_Is_Updated_After_Parent_Changeed() + public void HasMirrorTransform_Of_Children_Is_Updated_After_Parent_Changed() { - Control child; + var child = new Visual() + { + FlowDirection = FlowDirection.LeftToRight, + }; + var target = new Decorator { FlowDirection = FlowDirection.LeftToRight, - Child = child = new Control() - { - FlowDirection = FlowDirection.LeftToRight, - } }; + target.VisualChildren.Add(child); Assert.False(target.HasMirrorTransform); Assert.False(child.HasMirrorTransform); From 16f73a32c5760ec21f519c0e557ecdd5c48ab9c4 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 5 Dec 2022 10:23:55 +0200 Subject: [PATCH 18/40] Rmove BypassFlowDirectionPolicies from TopLavel --- src/Avalonia.Controls/TopLevel.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 59ad696148..d2d5a8309c 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -357,12 +357,6 @@ namespace Avalonia.Controls /// protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this); - public override void InvalidateMirrorTransform() - { - } - - protected override bool BypassFlowDirectionPolicies => true; - /// /// Handles a paint notification from . /// From 18aa1e16e51b914419bd9b344c056a641bfc7962 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 5 Dec 2022 10:38:46 +0200 Subject: [PATCH 19/40] Submodel --- nukebuild/Numerge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nukebuild/Numerge b/nukebuild/Numerge index fb92f917cd..aef10ae67d 160000 --- a/nukebuild/Numerge +++ b/nukebuild/Numerge @@ -1 +1 @@ -Subproject commit fb92f917cd2d3aaec0d2294635d922184ff1e0fc +Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5 From aa31daecde49ac4ec880a61dd9eb0cd2e6197eb9 Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Mon, 5 Dec 2022 13:46:11 +0200 Subject: [PATCH 20/40] Move FlowDirectionTests to Avalonia.Base.Tests --- .../FlowDirectionTests.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{Avalonia.Controls.UnitTests => Avalonia.Base.UnitTests}/FlowDirectionTests.cs (100%) diff --git a/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs b/tests/Avalonia.Base.UnitTests/FlowDirectionTests.cs similarity index 100% rename from tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs rename to tests/Avalonia.Base.UnitTests/FlowDirectionTests.cs From 58b5eeb9d21a074cd588bc3b58026a9ca87b0575 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Dec 2022 14:38:12 +0100 Subject: [PATCH 21/40] Added failing tests for #9561. --- .../Styling/StyledElementTests_Theming.cs | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index 672945cb24..b5a9b35134 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -359,16 +359,49 @@ public class StyledElementTests_Theming } [Fact] - public void Implicit_Theme_Is_Cleared_When_Removed_From_Logical_Tree() + public void Implicit_Theme_Is_Not_Detached_When_Removed_From_Logical_Tree() { var target = CreateTarget(); var root = CreateRoot(target); - - Assert.NotNull(target.GetEffectiveTheme()); + + Assert.Equal("theme", target.Tag); root.Child = null; - Assert.Null(target.GetEffectiveTheme()); + var border = Assert.IsType(target.VisualChild); + Assert.Equal("theme", target.Tag); + Assert.Equal("theme", border.Tag); + } + + [Fact] + public void Can_Attach_Then_Reattach_To_Same_Logical_Tree() + { + var target = CreateTarget(); + var root = CreateRoot(target); + + Assert.Equal("theme", target.Tag); + + root.Child = null; + root.Child = target; + + Assert.Equal("theme", target.Tag); + } + + [Fact] + public void Implicit_Theme_Is_Reevaluated_When_Removed_And_Added_To_Different_Logical_Tree() + { + var target = CreateTarget(); + var root1 = CreateRoot(target, "theme1"); + var root2 = CreateRoot(null, "theme2"); + + Assert.Equal("theme1", target.Tag); + + root1.Child = null; + root2.Child = target; + + var border = Assert.IsType(target.VisualChild); + Assert.Equal("theme2", target.Tag); + Assert.Equal("theme2", border.Tag); } [Fact] @@ -402,10 +435,10 @@ public class StyledElementTests_Theming private static ThemedControl CreateTarget() => new ThemedControl(); - private static TestRoot CreateRoot(Control child) + private static TestRoot CreateRoot(Control? child, string themeTag = "theme") { var result = new TestRoot(); - result.Resources.Add(typeof(ThemedControl), CreateTheme()); + result.Resources.Add(typeof(ThemedControl), CreateTheme(themeTag)); result.Child = child; result.LayoutManager.ExecuteInitialLayoutPass(); return result; @@ -530,7 +563,7 @@ public class StyledElementTests_Theming } } - private static ControlTheme CreateTheme() + private static ControlTheme CreateTheme(string tag = "theme") { var template = new FuncControlTemplate((o, n) => new Border()); @@ -539,7 +572,7 @@ public class StyledElementTests_Theming TargetType = typeof(ThemedControl), Setters = { - new Setter(Control.TagProperty, "theme"), + new Setter(Control.TagProperty, tag), new Setter(TemplatedControl.TemplateProperty, template), new Setter(TemplatedControl.CornerRadiusProperty, new CornerRadius(5)), }, @@ -550,7 +583,7 @@ public class StyledElementTests_Theming Setters = { new Setter(Border.BackgroundProperty, Brushes.Red), - new Setter(Control.TagProperty, "theme"), + new Setter(Control.TagProperty, tag), } }, new Style(x => x.Nesting().Class("foo").Template().OfType()) From b382a9d6ac1a4c237d891585f4f94e1d47d5abc9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Dec 2022 14:43:39 +0100 Subject: [PATCH 22/40] Re-evaluate theme on tree detach + reattach. Fixes #9561 --- src/Avalonia.Base/StyledElement.cs | 67 ++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 6fa2027e1e..6043175eee 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -81,6 +81,7 @@ namespace Avalonia private Styles? _styles; private bool _stylesApplied; private bool _themeApplied; + private bool _templatedParentThemeApplied; private AvaloniaObject? _templatedParent; private bool _dataContextUpdating; private ControlTheme? _implicitTheme; @@ -375,6 +376,12 @@ namespace Avalonia _themeApplied = true; } + if (!_templatedParentThemeApplied) + { + ApplyTemplatedParentControlTheme(); + _templatedParentThemeApplied = true; + } + if (!_stylesApplied) { ApplyStyles(this); @@ -613,26 +620,38 @@ namespace Avalonia base.OnPropertyChanged(change); if (change.Property == ThemeProperty) - { OnControlThemeChanged(); - _themeApplied = false; - } } private protected virtual void OnControlThemeChanged() { var values = GetValueStore(); values.BeginStyling(); - try { values.RemoveFrames(FrameType.Theme); } - finally { values.EndStyling(); } + + try + { + values.RemoveFrames(FrameType.Theme); + } + finally + { + values.EndStyling(); + _themeApplied = false; + } } internal virtual void OnTemplatedParentControlThemeChanged() { var values = GetValueStore(); values.BeginStyling(); - try { values.RemoveFrames(FrameType.TemplatedParentTheme); } - finally { values.EndStyling(); } + try + { + values.RemoveFrames(FrameType.TemplatedParentTheme); + } + finally + { + values.EndStyling(); + _templatedParentThemeApplied = false; + } } internal ControlTheme? GetEffectiveTheme() @@ -743,13 +762,13 @@ namespace Avalonia private void ApplyControlTheme() { - var theme = GetEffectiveTheme(); - - if (theme is not null) + if (GetEffectiveTheme() is { } theme) ApplyControlTheme(theme, FrameType.Theme); + } - if (TemplatedParent is StyledElement styleableParent && - styleableParent.GetEffectiveTheme() is { } parentTheme) + private void ApplyTemplatedParentControlTheme() + { + if ((TemplatedParent as StyledElement)?.GetEffectiveTheme() is { } parentTheme) { ApplyControlTheme(parentTheme, FrameType.TemplatedParentTheme); } @@ -793,6 +812,28 @@ namespace Avalonia ApplyStyle(child, host, type); } + private void ReevaluateImplicitTheme() + { + // We only need to check if the theme has changed when Theme isn't set (i.e. when we + // have an implicit theme). + if (Theme is not null) + return; + + // Refetch the implicit theme. + var oldImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme; + _implicitTheme = null; + GetEffectiveTheme(); + + var newImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme; + + // If the implicit theme has changed, detach the existing theme. + if (newImplicitTheme != oldImplicitTheme) + { + OnControlThemeChanged(); + _themeApplied = false; + } + } + private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e) { if (this.GetLogicalParent() == null && !(this is ILogicalRoot)) @@ -811,6 +852,7 @@ namespace Avalonia { _logicalRoot = e.Root; + ReevaluateImplicitTheme(); ApplyStyling(); NotifyResourcesChanged(propagate: false); @@ -835,7 +877,6 @@ namespace Avalonia if (_logicalRoot != null) { _logicalRoot = null; - _implicitTheme = null; InvalidateStyles(recurse: false); OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); From 387728fb2d0ab7019e22cd506851e3624c7ae469 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Dec 2022 16:17:08 +0100 Subject: [PATCH 23/40] Added failing test for animations without activator. Mentioned in #9561. --- .../Styling/StyleTests.cs | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs index 805b3e7aa6..4f5d1b8ec8 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs @@ -868,7 +868,53 @@ namespace Avalonia.Base.UnitTests.Styling } [Fact] - public void Animations_Should_Be_Activated_And_Deactivated() + public void Animations_Should_Be_Activated() + { + Style style = new Style(x => x.OfType()) + { + Animations = + { + new Avalonia.Animation.Animation + { + Duration = TimeSpan.FromSeconds(1), + Children = + { + new KeyFrame + { + Setters = + { + new Setter { Property = Class1.DoubleProperty, Value = 5.0 } + }, + }, + new KeyFrame + { + Setters = + { + new Setter { Property = Class1.DoubleProperty, Value = 10.0 } + }, + Cue = new Cue(1d) + } + }, + } + } + }; + + var clock = new TestClock(); + var target = new Class1 { Clock = clock }; + + StyleHelpers.TryAttach(style, target); + + Assert.Equal(0.0, target.Double); + + clock.Step(TimeSpan.Zero); + Assert.Equal(5.0, target.Double); + + clock.Step(TimeSpan.FromSeconds(0.5)); + Assert.Equal(7.5, target.Double); + } + + [Fact] + public void Animations_With_Trigger_Should_Be_Activated_And_Deactivated() { Style style = new Style(x => x.OfType().Class("foo")) { From a3e79d3127672748064a53b08963d98dd0665c4a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Dec 2022 16:18:10 +0100 Subject: [PATCH 24/40] Start animations in styles without activators. Fixes issue mentioned in #9561. --- src/Avalonia.Base/Styling/StyleInstance.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Base/Styling/StyleInstance.cs b/src/Avalonia.Base/Styling/StyleInstance.cs index 4985aa16c7..ca602167c0 100644 --- a/src/Avalonia.Base/Styling/StyleInstance.cs +++ b/src/Avalonia.Base/Styling/StyleInstance.cs @@ -70,6 +70,9 @@ namespace Avalonia.Styling _animationTrigger ??= new Subject(); foreach (var animation in _animations) animation.Apply(animatable, null, _animationTrigger); + + if (_activator is null) + _animationTrigger.OnNext(true); } } From 80e1e1eb4cfb544a89e12c07ae56a994bfb37490 Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Mon, 5 Dec 2022 18:53:24 +0200 Subject: [PATCH 25/40] Disabled controls should lose focus and don't accept keyboard events --- src/Avalonia.Base/Input/InputElement.cs | 18 +++++++++++++++ .../TextBoxTests.cs | 22 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index d0130258c3..60d8ef87a3 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -199,6 +199,7 @@ namespace Avalonia.Input private bool _isFocusVisible; private bool _isPointerOver; private GestureRecognizerCollection? _gestureRecognizers; + private bool _restoreFocus; /// /// Initializes static members of the class. @@ -442,6 +443,23 @@ namespace Avalonia.Input { SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); PseudoClasses.Set(":disabled", !value); + + if (!IsEffectivelyEnabled) + { + if (FocusManager.Instance?.Current == this) + { + _restoreFocus = true; + FocusManager.Instance?.Focus(null); + } + else + { + _restoreFocus = false; + } + } + else if (IsEffectivelyEnabled && _restoreFocus) + { + FocusManager.Instance?.Focus(this); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 6b9105ccb5..bd6d5d55e2 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -59,6 +59,28 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void TextBox_Should_Lose_Focus_When_Disabled() + { + using (UnitTestApplication.Start(FocusServices)) + { + var target = new TextBox + { + Template = CreateTemplate() + }; + + target.ApplyTemplate(); + + var root = new TestRoot() { Child = target }; + + target.Focus(); + Assert.True(target.IsFocused); + target.IsEnabled = false; + Assert.False(target.IsFocused); + Assert.False(target.IsEnabled); + } + } + [Fact] public void Opening_Context_Flyout_Does_not_Lose_Selection() { From a691e7c8fae35c34f9fdfed272f857a6f228e8af Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 5 Dec 2022 17:26:27 +0000 Subject: [PATCH 26/40] don't set key event to handled when selection changes with space or enter --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index f8951b3fa3..44fa78ac21 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -588,7 +588,7 @@ namespace Avalonia.Controls.Primitives } else if (e.Key == Key.Space || e.Key == Key.Enter) { - e.Handled = UpdateSelectionFromEventSource( + UpdateSelectionFromEventSource( e.Source, true, e.KeyModifiers.HasFlag(KeyModifiers.Shift), From 8b490152d49b1cc310faf86a2aebdc52fd5f1760 Mon Sep 17 00:00:00 2001 From: dif-sam <41672086+dif-sam@users.noreply.github.com> Date: Mon, 5 Dec 2022 23:49:13 +0400 Subject: [PATCH 27/40] Increase GenerateElement performance Use ability of the IRecyclingDataTemplate to speed up performance, when CellTemplate is an instance of DataTemplate type. --- src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 3ec78d6d6a..3649ec4308 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -64,9 +64,11 @@ namespace Avalonia.Controls protected override Control GenerateElement(DataGridCell cell, object dataItem) { - if(CellTemplate != null) + if (CellTemplate != null) { - return CellTemplate.Build(dataItem); + return (CellTemplate is IRecyclingDataTemplate recyclingDataTemplate) + ? recyclingDataTemplate.Build(dataItem, cell.Content as IControl) + : CellTemplate.Build(dataItem); } if (Design.IsDesignMode) { From ef535eeea6de0492c4ed342d8fa26a1090ccd8c7 Mon Sep 17 00:00:00 2001 From: dif-sam <41672086+dif-sam@users.noreply.github.com> Date: Tue, 6 Dec 2022 00:12:14 +0400 Subject: [PATCH 28/40] Switch IControl to Control --- src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 3649ec4308..24ae358dcc 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls if (CellTemplate != null) { return (CellTemplate is IRecyclingDataTemplate recyclingDataTemplate) - ? recyclingDataTemplate.Build(dataItem, cell.Content as IControl) + ? recyclingDataTemplate.Build(dataItem, cell.Content as Control) : CellTemplate.Build(dataItem); } if (Design.IsDesignMode) From 845fea9d1103eb0487335d55ff0488fe66d22c5f Mon Sep 17 00:00:00 2001 From: Kevin Ivarsen Date: Mon, 5 Dec 2022 14:36:33 -0800 Subject: [PATCH 29/40] Swap the figures used for expanded/collapsed groups in DataGrid to match the standard convention. --- src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index a3c0ed1d0c..fb4335f4de 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -6,8 +6,8 @@ M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z - M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z - M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z + M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z + M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z From bdceb1c3dff626173fbfdd4d24d5723b1977c37d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 5 Dec 2022 23:02:18 -0500 Subject: [PATCH 30/40] Do not require matching ctor, if top level element --- .../AvaloniaXamlIlRuntimeCompiler.cs | 2 +- .../Avalonia.Markup.Xaml.Loader/xamlil.github | 2 +- .../Xaml/BasicTests.cs | 23 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs index 28d5a55f86..2bb91e8a32 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs @@ -234,7 +234,7 @@ namespace Avalonia.Markup.Xaml.XamlIl parsedDocuments.Add(new XamlDocumentResource(parsed, document.BaseUri?.ToString(), null, null, builder, compiler.DefinePopulateMethod(builder, parsed, AvaloniaXamlIlCompiler.PopulateName, true), - compiler.DefineBuildMethod(builder, parsed, AvaloniaXamlIlCompiler.BuildName, true))); + document.RootInstance is null ? compiler.DefineBuildMethod(builder, parsed, AvaloniaXamlIlCompiler.BuildName, true) : null)); originalDocuments.Add(document); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index 491de981dd..b800d3e1fa 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit 491de981dd4433ee58bc9540e2cd4a5d168f8168 +Subproject commit b800d3e1fac0940c571bfa540e3af4dbd59f7dbb diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index c7afaee697..18a6dd9803 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -899,6 +899,17 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal("Foo", target.Text); } + [Fact] + public void Should_Parse_And_Populate_Type_Without_Public_Ctor() + { + var xaml = @""; + var target = (ObjectWithoutPublicCtor)AvaloniaRuntimeXamlLoader.Load(xaml, rootInstance: new ObjectWithoutPublicCtor("Hello")); + + Assert.NotNull(target); + Assert.Equal("World", target.Test2); + Assert.Equal("Hello", target.Test1); + } + private class SelectedItemsViewModel : INotifyPropertyChanged { public string[] Items { get; set; } @@ -928,6 +939,18 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Child = child; } } + + public class ObjectWithoutPublicCtor + { + public ObjectWithoutPublicCtor(string param) + { + Test1 = param; + } + + public string Test1 { get; set; } + + public string Test2 { get; set; } + } public class ObjectWithAddChildOfT : IAddChild, IAddChild { From 003923b50d635eadd18b979c98d9ec753630e5c6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Dec 2022 11:42:37 +0100 Subject: [PATCH 31/40] Fix integration tests on Windows. We need to be explicit about which element we're talking about when there are two elements with the same content, and so use accessibility IDs rather than Name. --- samples/IntegrationTestApp/MainWindow.axaml | 10 +++++----- samples/IntegrationTestApp/ShowWindowTest.axaml | 8 ++++---- .../WindowTests.cs | 16 ++++++++-------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 801ff765c8..54c0cb0655 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -24,7 +24,7 @@ WindowState: - + @@ -133,10 +133,10 @@ CenterOwner - Normal - Minimized - Maximized - FullScreen + Normal + Minimized + Maximized + FullScreen diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index c3a0d8d2e2..00987429d0 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -27,10 +27,10 @@ - Normal - Minimized - Maximized - FullScreen + Normal + Minimized + Maximized + FullScreen diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index b6ac75b78e..4d833cdb1f 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -90,7 +90,7 @@ namespace Avalonia.IntegrationTests.Appium try { _session.FindElementByAccessibilityId("WindowState").SendClick(); - _session.FindElementByName("Normal").SendClick(); + _session.FindElementByAccessibilityId("WindowStateNormal").SendClick(); // Wait for animations to run. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) @@ -155,11 +155,11 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("Normal", windowState.GetComboBoxValue()); windowState.Click(); - _session.FindElementByName("Maximized").SendClick(); + _session.FindElementByAccessibilityId("WindowStateMaximized").SendClick(); Assert.Equal("Maximized", windowState.GetComboBoxValue()); windowState.Click(); - _session.FindElementByName("Normal").SendClick(); + _session.FindElementByAccessibilityId("WindowStateNormal").SendClick(); var current = GetWindowInfo(); Assert.Equal(original.Position, current.Position); @@ -169,7 +169,7 @@ namespace Avalonia.IntegrationTests.Appium if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || mode == ShowWindowMode.NonOwned) { windowState.Click(); - _session.FindElementByName("FullScreen").SendClick(); + _session.FindElementByAccessibilityId("WindowStateFullScreen").SendClick(); Assert.Equal("FullScreen", windowState.GetComboBoxValue()); current = GetWindowInfo(); @@ -179,7 +179,7 @@ namespace Avalonia.IntegrationTests.Appium windowState.SendClick(); - _session.FindElementByName("Normal").SendClick(); + _session.FindElementByAccessibilityId("WindowStateNormal").SendClick(); current = GetWindowInfo(); Assert.Equal(original.Position, current.Position); @@ -223,13 +223,13 @@ namespace Avalonia.IntegrationTests.Appium // Not sure how to handle testing minimized windows currently. if (state == Controls.WindowState.Minimized) continue; - + // Child/Modal windows cannot be fullscreen on macOS. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && state == Controls.WindowState.FullScreen && mode != ShowWindowMode.NonOwned) continue; - + data.Add(size, mode, state); } } @@ -286,7 +286,7 @@ namespace Avalonia.IntegrationTests.Appium _session.FindElementByName(location.ToString()).SendClick(); stateComboBox.Click(); - _session.FindElementByName(state.ToString()).SendClick(); + _session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick(); return showButton.OpenWindowWithClick(); } From 23e45beb906fff790715a530ee1d2663601dbff3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Dec 2022 12:00:25 +0100 Subject: [PATCH 32/40] Fix integration tests on macOS. --- .../Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 8f4417a451..6c61a85561 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -128,7 +128,7 @@ namespace Avalonia.IntegrationTests.Appium Thread.Sleep(1000); // Make sure we entered fullscreen. - var windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + var windowState = mainWindow.FindElementByAccessibilityId("MainWindowState"); Assert.Equal("FullScreen", windowState.Text); // Open child window. @@ -147,7 +147,7 @@ namespace Avalonia.IntegrationTests.Appium // Make sure we exited fullscreen. mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + windowState = mainWindow.FindElementByAccessibilityId("MainWindowState"); Assert.Equal("Normal", windowState.Text); } @@ -169,7 +169,7 @@ namespace Avalonia.IntegrationTests.Appium // Make sure we entered fullscreen. mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + windowState = mainWindow.FindElementByAccessibilityId("MainWindowState"); Assert.Equal("FullScreen", windowState.Text); // Click on main window @@ -185,7 +185,7 @@ namespace Avalonia.IntegrationTests.Appium // Make sure we exited fullscreen. mainWindow = _session.FindElementByAccessibilityId("MainWindow"); - windowState = mainWindow.FindElementByAccessibilityId("WindowState"); + windowState = mainWindow.FindElementByAccessibilityId("MainWindowState"); Assert.Equal("Normal", windowState.Text); } From ba1c4dcc1b4b468c32859233a9898cbcfa525b55 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 6 Dec 2022 07:19:55 -0500 Subject: [PATCH 33/40] Switch to upstream xamlx --- src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index b800d3e1fa..5e498f8bcc 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit b800d3e1fac0940c571bfa540e3af4dbd59f7dbb +Subproject commit 5e498f8bcca403a34aff5efc825cbb4e12b7fa8e From 4502ed3729663199ec173f79cd1373e12855d28f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Dec 2022 16:29:43 +0100 Subject: [PATCH 34/40] Added ReactiveUIDemo sample. --- .ncrunch/ReactiveUIDemo.v3.ncrunchproject | 5 +++ Avalonia.Desktop.slnf | 1 + Avalonia.sln | 7 ++++ samples/ReactiveUIDemo/App.axaml | 8 ++++ samples/ReactiveUIDemo/App.axaml.cs | 37 +++++++++++++++++++ samples/ReactiveUIDemo/MainWindow.axaml | 19 ++++++++++ samples/ReactiveUIDemo/MainWindow.axaml.cs | 22 +++++++++++ samples/ReactiveUIDemo/ReactiveUIDemo.csproj | 28 ++++++++++++++ .../ReactiveUIDemo/ViewModels/BarViewModel.cs | 11 ++++++ .../ReactiveUIDemo/ViewModels/FooViewModel.cs | 11 ++++++ .../ViewModels/MainWindowViewModel.cs | 9 +++++ .../ViewModels/RoutedViewHostPageViewModel.cs | 21 +++++++++++ samples/ReactiveUIDemo/Views/BarView.axaml | 16 ++++++++ samples/ReactiveUIDemo/Views/BarView.axaml.cs | 28 ++++++++++++++ samples/ReactiveUIDemo/Views/FooView.axaml | 16 ++++++++ samples/ReactiveUIDemo/Views/FooView.axaml.cs | 28 ++++++++++++++ 16 files changed, 267 insertions(+) create mode 100644 .ncrunch/ReactiveUIDemo.v3.ncrunchproject create mode 100644 samples/ReactiveUIDemo/App.axaml create mode 100644 samples/ReactiveUIDemo/App.axaml.cs create mode 100644 samples/ReactiveUIDemo/MainWindow.axaml create mode 100644 samples/ReactiveUIDemo/MainWindow.axaml.cs create mode 100644 samples/ReactiveUIDemo/ReactiveUIDemo.csproj create mode 100644 samples/ReactiveUIDemo/ViewModels/BarViewModel.cs create mode 100644 samples/ReactiveUIDemo/ViewModels/FooViewModel.cs create mode 100644 samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs create mode 100644 samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs create mode 100644 samples/ReactiveUIDemo/Views/BarView.axaml create mode 100644 samples/ReactiveUIDemo/Views/BarView.axaml.cs create mode 100644 samples/ReactiveUIDemo/Views/FooView.axaml create mode 100644 samples/ReactiveUIDemo/Views/FooView.axaml.cs diff --git a/.ncrunch/ReactiveUIDemo.v3.ncrunchproject b/.ncrunch/ReactiveUIDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/ReactiveUIDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 6ba05332be..3fa8e969c8 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -9,6 +9,7 @@ "samples\\MiniMvvm\\MiniMvvm.csproj", "samples\\SampleControls\\ControlSamples.csproj", "samples\\Sandbox\\Sandbox.csproj", + "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj", "src\\Avalonia.Base\\Avalonia.Base.csproj", "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj", "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index 34b5596119..63b331ec08 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -230,6 +230,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser", "s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -541,6 +543,10 @@ Global {90B08091-9BBD-4362-B712-E9F2CC62B218}.Debug|Any CPU.Build.0 = Debug|Any CPU {90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.ActiveCfg = Release|Any CPU {90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.Build.0 = Release|Any CPU + {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -605,6 +611,7 @@ Global {47F8530C-F19B-4B1A-B4D6-EB231522AE5D} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} {15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098} {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/samples/ReactiveUIDemo/App.axaml b/samples/ReactiveUIDemo/App.axaml new file mode 100644 index 0000000000..dd3a39f6ac --- /dev/null +++ b/samples/ReactiveUIDemo/App.axaml @@ -0,0 +1,8 @@ + + + + + diff --git a/samples/ReactiveUIDemo/App.axaml.cs b/samples/ReactiveUIDemo/App.axaml.cs new file mode 100644 index 0000000000..4578566427 --- /dev/null +++ b/samples/ReactiveUIDemo/App.axaml.cs @@ -0,0 +1,37 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using ReactiveUI; +using ReactiveUIDemo.ViewModels; +using ReactiveUIDemo.Views; +using Splat; + +namespace ReactiveUIDemo +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + Locator.CurrentMutable.Register(() => new FooView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new BarView(), typeof(IViewFor)); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + desktop.MainWindow = new MainWindow(); + base.OnFrameworkInitializationCompleted(); + } + + public static int Main(string[] args) + => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .UseReactiveUI() + .LogToTrace(); + } +} diff --git a/samples/ReactiveUIDemo/MainWindow.axaml b/samples/ReactiveUIDemo/MainWindow.axaml new file mode 100644 index 0000000000..7775fc5a79 --- /dev/null +++ b/samples/ReactiveUIDemo/MainWindow.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/samples/ReactiveUIDemo/MainWindow.axaml.cs b/samples/ReactiveUIDemo/MainWindow.axaml.cs new file mode 100644 index 0000000000..5bf2d476fd --- /dev/null +++ b/samples/ReactiveUIDemo/MainWindow.axaml.cs @@ -0,0 +1,22 @@ +using ReactiveUIDemo.ViewModels; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ReactiveUIDemo +{ + public class MainWindow : Window + { + public MainWindow() + { + this.InitializeComponent(); + this.DataContext = new MainWindowViewModel(); + this.AttachDevTools(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj new file mode 100644 index 0000000000..94ca4ee809 --- /dev/null +++ b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj @@ -0,0 +1,28 @@ + + + Exe + net6.0 + enable + + + + + + + + + + + BarView.axaml + + + FooView.axaml + + + + + + + + + diff --git a/samples/ReactiveUIDemo/ViewModels/BarViewModel.cs b/samples/ReactiveUIDemo/ViewModels/BarViewModel.cs new file mode 100644 index 0000000000..3448453d81 --- /dev/null +++ b/samples/ReactiveUIDemo/ViewModels/BarViewModel.cs @@ -0,0 +1,11 @@ +using ReactiveUI; + +namespace ReactiveUIDemo.ViewModels +{ + internal class BarViewModel : ReactiveObject, IRoutableViewModel + { + public BarViewModel(IScreen screen) => HostScreen = screen; + public string UrlPathSegment => "Bar"; + public IScreen HostScreen { get; } + } +} diff --git a/samples/ReactiveUIDemo/ViewModels/FooViewModel.cs b/samples/ReactiveUIDemo/ViewModels/FooViewModel.cs new file mode 100644 index 0000000000..1a363e18dc --- /dev/null +++ b/samples/ReactiveUIDemo/ViewModels/FooViewModel.cs @@ -0,0 +1,11 @@ +using ReactiveUI; + +namespace ReactiveUIDemo.ViewModels +{ + internal class FooViewModel : ReactiveObject, IRoutableViewModel + { + public FooViewModel(IScreen screen) => HostScreen = screen; + public string UrlPathSegment => "Foo"; + public IScreen HostScreen { get; } + } +} diff --git a/samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs b/samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000000..2222137d38 --- /dev/null +++ b/samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,9 @@ +using ReactiveUI; + +namespace ReactiveUIDemo.ViewModels +{ + internal class MainWindowViewModel : ReactiveObject + { + public RoutedViewHostPageViewModel RoutedViewHost { get; } = new(); + } +} diff --git a/samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs b/samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs new file mode 100644 index 0000000000..701447cfe8 --- /dev/null +++ b/samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs @@ -0,0 +1,21 @@ +using ReactiveUI; + +namespace ReactiveUIDemo.ViewModels +{ + internal class RoutedViewHostPageViewModel : ReactiveObject, IScreen + { + public RoutedViewHostPageViewModel() + { + Foo = new(this); + Bar = new(this); + Router.Navigate.Execute(Foo); + } + + public RoutingState Router { get; } = new(); + public FooViewModel Foo { get; } + public BarViewModel Bar { get; } + + public void ShowFoo() => Router.Navigate.Execute(Foo); + public void ShowBar() => Router.Navigate.Execute(Bar); + } +} diff --git a/samples/ReactiveUIDemo/Views/BarView.axaml b/samples/ReactiveUIDemo/Views/BarView.axaml new file mode 100644 index 0000000000..2622245997 --- /dev/null +++ b/samples/ReactiveUIDemo/Views/BarView.axaml @@ -0,0 +1,16 @@ + + + + Bar! + + + diff --git a/samples/ReactiveUIDemo/Views/BarView.axaml.cs b/samples/ReactiveUIDemo/Views/BarView.axaml.cs new file mode 100644 index 0000000000..2fbea6de91 --- /dev/null +++ b/samples/ReactiveUIDemo/Views/BarView.axaml.cs @@ -0,0 +1,28 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ReactiveUI; +using ReactiveUIDemo.ViewModels; + +namespace ReactiveUIDemo.Views +{ + internal partial class BarView : UserControl, IViewFor + { + public BarView() + { + InitializeComponent(); + } + + public BarViewModel? ViewModel { get; set; } + + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (BarViewModel?)value; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ReactiveUIDemo/Views/FooView.axaml b/samples/ReactiveUIDemo/Views/FooView.axaml new file mode 100644 index 0000000000..8f73250d3b --- /dev/null +++ b/samples/ReactiveUIDemo/Views/FooView.axaml @@ -0,0 +1,16 @@ + + + + Foo! + + + diff --git a/samples/ReactiveUIDemo/Views/FooView.axaml.cs b/samples/ReactiveUIDemo/Views/FooView.axaml.cs new file mode 100644 index 0000000000..313a71044c --- /dev/null +++ b/samples/ReactiveUIDemo/Views/FooView.axaml.cs @@ -0,0 +1,28 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ReactiveUI; +using ReactiveUIDemo.ViewModels; + +namespace ReactiveUIDemo.Views +{ + internal partial class FooView : UserControl, IViewFor + { + public FooView() + { + InitializeComponent(); + } + + public FooViewModel? ViewModel { get; set; } + + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (FooViewModel?)value; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} From f4eeb82a58100bb23554ea40f37dbd46431151e3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Dec 2022 16:43:51 +0100 Subject: [PATCH 35/40] Actually dispose DisposeAnimationInstanceSubject. We were creating a `DisposeAnimationInstanceSubject` and then just discarding it, meaning it was never disposed. Fixes #6111 Fixes #9514 --- src/Avalonia.Base/Animation/Animators/Animator`1.cs | 3 ++- .../Animation/AnimationIterationTests.cs | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Animation/Animators/Animator`1.cs b/src/Avalonia.Base/Animation/Animators/Animator`1.cs index 8765cfb4c9..b5d1feb4a7 100644 --- a/src/Avalonia.Base/Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Base/Animation/Animators/Animator`1.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Animation.Utils; using Avalonia.Collections; @@ -39,7 +40,7 @@ namespace Avalonia.Animation.Animators VerifyConvertKeyFrames(); var subject = new DisposeAnimationInstanceSubject(this, animation, control, clock, onComplete); - return match.Subscribe(subject); + return new CompositeDisposable(match.Subscribe(subject), subject); } protected T InterpolationHandler(double animationTime, T neutralValue) diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs index ca24b95e65..58e908aca9 100644 --- a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs @@ -181,7 +181,7 @@ namespace Avalonia.Base.UnitTests.Animation Assert.Equal(border.Width, 300d); } - [Fact(Skip = "See #6111")] + [Fact] public void Dispose_Subscription_Should_Stop_Animation() { var keyframe1 = new KeyFrame() @@ -310,7 +310,7 @@ namespace Avalonia.Base.UnitTests.Animation Assert.True(animationRun.IsCompleted); } - [Fact(Skip = "See #6111")] + [Fact] public void Cancellation_Should_Stop_Animation() { var keyframe1 = new KeyFrame() @@ -372,7 +372,6 @@ namespace Avalonia.Base.UnitTests.Animation clock.Step(TimeSpan.FromSeconds(1)); clock.Step(TimeSpan.FromSeconds(2)); clock.Step(TimeSpan.FromSeconds(3)); - //Assert.Equal(2, propertyChangedCount); animationRun.Wait(); From 6b8b5d78f491b35a40650d66727671614b4b64c2 Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Tue, 6 Dec 2022 19:10:37 +0200 Subject: [PATCH 36/40] Rework focus so we don't return focus to the lastly enabled element --- src/Avalonia.Base/Input/InputElement.cs | 17 ++--------- .../Input/InputElement_Focus.cs | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index 60d8ef87a3..fa755277cc 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -199,7 +199,6 @@ namespace Avalonia.Input private bool _isFocusVisible; private bool _isPointerOver; private GestureRecognizerCollection? _gestureRecognizers; - private bool _restoreFocus; /// /// Initializes static members of the class. @@ -444,21 +443,9 @@ namespace Avalonia.Input SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); PseudoClasses.Set(":disabled", !value); - if (!IsEffectivelyEnabled) + if (!IsEffectivelyEnabled && FocusManager.Instance?.Current == this) { - if (FocusManager.Instance?.Current == this) - { - _restoreFocus = true; - FocusManager.Instance?.Focus(null); - } - else - { - _restoreFocus = false; - } - } - else if (IsEffectivelyEnabled && _restoreFocus) - { - FocusManager.Instance?.Focus(this); + FocusManager.Instance?.Focus(null); } } } diff --git a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs index 2d8ee62ef2..e36ce21009 100644 --- a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs +++ b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs @@ -1,5 +1,7 @@ +using System; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.UnitTests; using Xunit; @@ -25,6 +27,33 @@ namespace Avalonia.Base.UnitTests.Input } } + [Fact] + public void Focus_Should_Not_Get_Restored_To_Enabled_Control() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var sp = new StackPanel(); + Button target = new Button(); + Button target1 = new Button(); + target.Click += (s, e) => target.IsEnabled = false; + target1.Click += (s, e) => target.IsEnabled = true; + sp.Children.Add(target); + sp.Children.Add(target1); + var root = new TestRoot + { + Child = sp + }; + + target.Focus(); + target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent)); + Assert.False(target.IsEnabled); + Assert.False(target.IsFocused); + target1.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent)); + Assert.True(target.IsEnabled); + Assert.False(target.IsFocused); + } + } + [Fact] public void Focus_Should_Be_Cleared_When_Control_Is_Removed_From_VisualTree() { From 0f8b1d6fa387d2f6c9f64ea24506764cc73d46cb Mon Sep 17 00:00:00 2001 From: Adir Hudayfi Date: Tue, 6 Dec 2022 21:25:40 +0200 Subject: [PATCH 37/40] Fixed key frame animations not getting properly cleaned up --- .../Composition/Animations/KeyFrameAnimationInstance.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs index 570c6a6d07..570b852108 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -177,6 +177,7 @@ namespace Avalonia.Rendering.Composition.Animations public override void Deactivate() { TargetObject.Compositor.RemoveFromClock(this); + _finished = false; base.Deactivate(); } } From 19ed6ec05f8d472e06efcc8bf1d99f07bff40566 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 7 Dec 2022 10:03:16 +0000 Subject: [PATCH 38/40] addressed reveiw --- .../PullGestureRecognizer.cs | 10 +- .../RefreshCompletionDeferral.cs | 36 ++++++ .../RefreshRequestedEventArgs.cs | 42 +++++++ .../PullToRefresh/RefreshVisualizer.cs | 110 ++---------------- .../RefreshVisualizerOrientation.cs | 13 +++ .../PullToRefresh/RefreshVisualizerState.cs | 14 +++ ...ScrollViewerIRefreshInfoProviderAdapter.cs | 4 +- .../Controls/RefreshContainer.xaml | 2 +- .../Controls/RefreshContainer.xaml | 2 +- 9 files changed, 122 insertions(+), 111 deletions(-) rename src/Avalonia.Base/Input/{ => GestureRecognizers}/PullGestureRecognizer.cs (93%) create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs create mode 100644 src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs diff --git a/src/Avalonia.Base/Input/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs similarity index 93% rename from src/Avalonia.Base/Input/PullGestureRecognizer.cs rename to src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs index bbbded44fa..fedd07ec32 100644 --- a/src/Avalonia.Base/Input/PullGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs @@ -60,9 +60,9 @@ namespace Avalonia.Input public void PointerMoved(PointerEventArgs e) { - if (_tracking == e.Pointer) + if (_tracking == e.Pointer && _target is Visual visual) { - var currentPosition = e.GetPosition(_target); + var currentPosition = e.GetPosition(visual); _actions!.Capture(e.Pointer, this); Vector delta = default; @@ -100,13 +100,13 @@ namespace Avalonia.Input public void PointerPressed(PointerPressedEventArgs e) { - if (_target != null && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) { - var position = e.GetPosition(_target); + var position = e.GetPosition(visual); var canPull = false; - var bounds = _target.Bounds; + var bounds = visual.Bounds; switch (PullDirection) { diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs b/src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs new file mode 100644 index 0000000000..a18b3c2934 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; + +namespace Avalonia.Controls +{ + /// + /// Deferral class for notify that a work done in RefreshRequested event is done. + /// + public class RefreshCompletionDeferral + { + private Action _deferredAction; + private int _deferCount; + + public RefreshCompletionDeferral(Action deferredAction) + { + _deferredAction = deferredAction; + } + + public void Complete() + { + Interlocked.Decrement(ref _deferCount); + + if (_deferCount == 0) + { + _deferredAction?.Invoke(); + } + } + + public RefreshCompletionDeferral Get() + { + Interlocked.Increment(ref _deferCount); + + return this; + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs b/src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs new file mode 100644 index 0000000000..4bb25d3b2c --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs @@ -0,0 +1,42 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// Provides event data for RefreshRequested events. + /// + public class RefreshRequestedEventArgs : RoutedEventArgs + { + private RefreshCompletionDeferral _refreshCompletionDeferral; + + /// + /// Gets a deferral object for managing the work done in the RefreshRequested event handler. + /// + /// A object + public RefreshCompletionDeferral GetDeferral() + { + return _refreshCompletionDeferral.Get(); + } + + public RefreshRequestedEventArgs(Action deferredAction, RoutedEvent? routedEvent) : base(routedEvent) + { + _refreshCompletionDeferral = new RefreshCompletionDeferral(deferredAction); + } + + public RefreshRequestedEventArgs(RefreshCompletionDeferral completionDeferral, RoutedEvent? routedEvent) : base(routedEvent) + { + _refreshCompletionDeferral = completionDeferral; + } + + internal void IncrementCount() + { + _refreshCompletionDeferral?.Get(); + } + + internal void DecrementCount() + { + _refreshCompletionDeferral?.Complete(); + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index 2647ac1ac1..f2f735aaa9 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -1,8 +1,6 @@ using System; using System.Numerics; using System.Reactive.Linq; -using System.Threading; -using Avalonia.Animation; using Avalonia.Animation.Easings; using Avalonia.Controls.Primitives; using Avalonia.Controls.PullToRefresh; @@ -104,14 +102,7 @@ namespace Avalonia.Controls internal PullDirection PullDirection { get => GetValue(PullDirectionProperty); - set - { - SetValue(PullDirectionProperty, value); - - OnOrientationChanged(); - - UpdateContent(); - } + set => SetValue(PullDirectionProperty, value); } internal RefreshInfoProvider? RefreshInfoProvider @@ -201,7 +192,7 @@ namespace Avalonia.Controls } else { - RaisePropertyChanged(ContentProperty, null, Content); + RaisePropertyChanged(ContentProperty, null, Content, Data.BindingPriority.Style, false); } } @@ -400,6 +391,12 @@ namespace Avalonia.Controls break; } + UpdateContent(); + } + else if(change.Property == PullDirectionProperty) + { + OnOrientationChanged(); + UpdateContent(); } } @@ -553,95 +550,4 @@ namespace Avalonia.Controls } } } - - /// - /// Defines constants that specify the state of a RefreshVisualizer - /// - public enum RefreshVisualizerState - { - Idle, - Peeking, - Interacting, - Pending, - Refreshing - } - - /// - /// Defines constants that specify the orientation of a RefreshVisualizer. - /// - public enum RefreshVisualizerOrientation - { - Auto, - Normal, - Rotate90DegreesCounterclockwise, - Rotate270DegreesCounterclockwise - } - - /// - /// Provides event data for RefreshRequested events. - /// - public class RefreshRequestedEventArgs : RoutedEventArgs - { - private RefreshCompletionDeferral _refreshCompletionDeferral; - - /// - /// Gets a deferral object for managing the work done in the RefreshRequested event handler. - /// - /// A object - public RefreshCompletionDeferral GetDeferral() - { - return _refreshCompletionDeferral.Get(); - } - - public RefreshRequestedEventArgs(Action deferredAction, RoutedEvent? routedEvent) : base(routedEvent) - { - _refreshCompletionDeferral = new RefreshCompletionDeferral(deferredAction); - } - - public RefreshRequestedEventArgs(RefreshCompletionDeferral completionDeferral, RoutedEvent? routedEvent) : base(routedEvent) - { - _refreshCompletionDeferral = completionDeferral; - } - - internal void IncrementCount() - { - _refreshCompletionDeferral?.Get(); - } - - internal void DecrementCount() - { - _refreshCompletionDeferral?.Complete(); - } - } - - /// - /// Deferral class for notify that a work done in RefreshRequested event is done. - /// - public class RefreshCompletionDeferral - { - private Action _deferredAction; - private int _deferCount; - - public RefreshCompletionDeferral(Action deferredAction) - { - _deferredAction = deferredAction; - } - - public void Complete() - { - Interlocked.Decrement(ref _deferCount); - - if (_deferCount == 0) - { - _deferredAction?.Invoke(); - } - } - - public RefreshCompletionDeferral Get() - { - Interlocked.Increment(ref _deferCount); - - return this; - } - } } diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs new file mode 100644 index 0000000000..1ea37f67b9 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs @@ -0,0 +1,13 @@ +namespace Avalonia.Controls +{ + /// + /// Defines constants that specify the orientation of a RefreshVisualizer. + /// + public enum RefreshVisualizerOrientation + { + Auto, + Normal, + Rotate90DegreesCounterclockwise, + Rotate270DegreesCounterclockwise + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs new file mode 100644 index 0000000000..5ab52f4de6 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs @@ -0,0 +1,14 @@ +namespace Avalonia.Controls +{ + /// + /// Defines constants that specify the state of a RefreshVisualizer + /// + public enum RefreshVisualizerState + { + Idle, + Peeking, + Interacting, + Pending, + Refreshing + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs index db06de69ad..c3aebc82c5 100644 --- a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs +++ b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.PullToRefresh _refreshPullDirection = pullDirection; } - public RefreshInfoProvider? AdaptFromTree(IVisual root, Size? refreshVIsualizerSize) + public RefreshInfoProvider? AdaptFromTree(Visual root, Size? refreshVIsualizerSize) { if (root is ScrollViewer scrollViewer) { @@ -45,7 +45,7 @@ namespace Avalonia.Controls.PullToRefresh } } - ScrollViewer? AdaptFromTreeRecursiveHelper(IVisual root, int depth) + ScrollViewer? AdaptFromTreeRecursiveHelper(Visual root, int depth) { if (depth == 0) { diff --git a/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml b/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml index 97002c25bd..8e29e6208f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml @@ -4,7 +4,7 @@ TargetType="RefreshContainer"> - + - + Date: Wed, 7 Dec 2022 22:28:59 +0000 Subject: [PATCH 39/40] use disposable pattern to make logic more robust. --- src/Avalonia.Controls/Window.cs | 34 +++++------------- src/Avalonia.Controls/WindowBase.cs | 56 ++++++++++++++++------------- 2 files changed, 39 insertions(+), 51 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 729bbaa4b3..a893c74324 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -567,10 +567,8 @@ namespace Avalonia.Controls /// public override void Hide() { - try + using (FreezeVisibilityChangeHandling()) { - IgnoreVisibilityChange = true; - if (!_shown) { return; @@ -596,10 +594,6 @@ namespace Avalonia.Controls IsVisible = false; _shown = false; } - finally - { - IgnoreVisibilityChange = false; - } } /// @@ -615,7 +609,7 @@ namespace Avalonia.Controls protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) { - if (!IgnoreVisibilityChange) + if (!IgnoreVisibilityChanges) { var isVisible = e.GetNewValue(); @@ -685,12 +679,10 @@ namespace Avalonia.Controls private void ShowCore(Window? owner) { - try + using (FreezeVisibilityChangeHandling()) { - IgnoreVisibilityChange = true; - EnsureStateBeforeShow(); - + if (owner != null) { EnsureParentStateBeforeShow(owner); @@ -733,10 +725,6 @@ namespace Avalonia.Controls Renderer?.Start(); OnOpened(EventArgs.Empty); } - finally - { - IgnoreVisibilityChange = false; - } } /// @@ -766,19 +754,17 @@ namespace Avalonia.Controls /// public Task ShowDialog(Window owner) { - try + using (FreezeVisibilityChangeHandling()) { - IgnoreVisibilityChange = true; - EnsureStateBeforeShow(); - + if (owner == null) { throw new ArgumentNullException(nameof(owner)); } - + EnsureParentStateBeforeShow(owner); - + if (_shown) { throw new InvalidOperationException("The window is already being shown."); @@ -828,10 +814,6 @@ namespace Avalonia.Controls OnOpened(EventArgs.Empty); return result.Task; } - finally - { - IgnoreVisibilityChange = false; - } } private void UpdateEnabled() diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 46653c8203..b71dc6df44 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -42,9 +42,11 @@ namespace Avalonia.Controls private bool _hasExecutedInitialLayoutPass; private bool _isActive; - protected bool IgnoreVisibilityChange { get; set; } + private int _ignoreVisibilityChanges; private WindowBase? _owner; + protected bool IgnoreVisibilityChanges => _ignoreVisibilityChanges > 0; + static WindowBase() { IsVisibleProperty.OverrideDefaultValue(false); @@ -66,6 +68,11 @@ namespace Avalonia.Controls impl.PositionChanged = HandlePositionChanged; } + protected IDisposable FreezeVisibilityChangeHandling() + { + return new IgnoreVisibilityChangesDisposable(this); + } + /// /// Fired when the window is activated. /// @@ -125,18 +132,12 @@ namespace Avalonia.Controls /// public virtual void Hide() { - IgnoreVisibilityChange = true; - - try + using (FreezeVisibilityChangeHandling()) { Renderer?.Stop(); PlatformImpl?.Hide(); IsVisible = false; } - finally - { - IgnoreVisibilityChange = false; - } } /// @@ -144,9 +145,7 @@ namespace Avalonia.Controls /// public virtual void Show() { - IgnoreVisibilityChange = true; - - try + using (FreezeVisibilityChangeHandling()) { EnsureInitialized(); ApplyStyling(); @@ -157,14 +156,11 @@ namespace Avalonia.Controls LayoutManager.ExecuteInitialLayoutPass(); _hasExecutedInitialLayoutPass = true; } + PlatformImpl?.Show(true, false); Renderer?.Start(); OnOpened(EventArgs.Empty); } - finally - { - IgnoreVisibilityChange = false; - } } /// @@ -202,23 +198,17 @@ namespace Avalonia.Controls protected override void HandleClosed() { - IgnoreVisibilityChange = true; - - try + using (FreezeVisibilityChangeHandling()) { IsVisible = false; - + if (this is IFocusScope scope) { FocusManager.Instance?.RemoveFocusScope(scope); } - + base.HandleClosed(); } - finally - { - IgnoreVisibilityChange = false; - } } /// @@ -320,7 +310,7 @@ namespace Avalonia.Controls protected virtual void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) { - if (!IgnoreVisibilityChange) + if (_ignoreVisibilityChanges == 0) { if ((bool)e.NewValue!) { @@ -332,5 +322,21 @@ namespace Avalonia.Controls } } } + + private readonly struct IgnoreVisibilityChangesDisposable : IDisposable + { + private readonly WindowBase _windowBase; + + public IgnoreVisibilityChangesDisposable(WindowBase windowBase) + { + _windowBase = windowBase; + _windowBase._ignoreVisibilityChanges++; + } + + public void Dispose() + { + _windowBase._ignoreVisibilityChanges--; + } + } } } From 98d53bb3f5fffde2ca8778127a15a8a3707b6deb Mon Sep 17 00:00:00 2001 From: Adir Hudayfi Date: Thu, 8 Dec 2022 17:02:59 +0200 Subject: [PATCH 40/40] Moved finished check to Activate --- .../Composition/Animations/KeyFrameAnimationInstance.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs index 570b852108..0117fe0713 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs @@ -170,6 +170,10 @@ namespace Avalonia.Rendering.Composition.Animations public override void Activate() { + if (_finished) + { + return; + } TargetObject.Compositor.AddToClock(this); base.Activate(); } @@ -177,7 +181,6 @@ namespace Avalonia.Rendering.Composition.Animations public override void Deactivate() { TargetObject.Compositor.RemoveFromClock(this); - _finished = false; base.Deactivate(); } }