From 9501da85fa4da89dc0d7e11eaff9316d42da523c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 23 Jan 2019 09:19:27 +0100 Subject: [PATCH 01/17] Added failing tests for #2260. --- .../Avalonia.Controls.UnitTests/ImageTests.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ImageTests.cs b/tests/Avalonia.Controls.UnitTests/ImageTests.cs index e92fc572b4..71d0d1e328 100644 --- a/tests/Avalonia.Controls.UnitTests/ImageTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ImageTests.cs @@ -61,5 +61,61 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(50, 50), target.DesiredSize); } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_No_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.None; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 100, 400)); + + Assert.Equal(new Size(50, 100), target.Bounds.Size); + } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_Fill_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.Fill; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 25, 100)); + + Assert.Equal(new Size(25, 100), target.Bounds.Size); + } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_Uniform_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.Uniform; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 25, 100)); + + Assert.Equal(new Size(25, 50), target.Bounds.Size); + } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_UniformToFill_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.UniformToFill; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 25, 100)); + + Assert.Equal(new Size(25, 100), target.Bounds.Size); + } } } From 3ee48b25e4ecd967227694f96a85a89aa36b4217 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 23 Jan 2019 09:09:44 +0100 Subject: [PATCH 02/17] Fix image arrange. `Image` was calculating its desired size correctly according to the value of `Stretch` but then was not applying that calculation to the arrange pass. Fixes #2260 --- src/Avalonia.Controls/Image.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 72379e7b53..c696fe7975 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -99,5 +99,22 @@ namespace Avalonia.Controls return new Size(); } } + + /// + protected override Size ArrangeOverride(Size finalSize) + { + var source = Source; + + if (source != null) + { + var sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); + var result = Stretch.CalculateSize(finalSize, sourceSize); + return result; + } + else + { + return new Size(); + } + } } } From af83673f918373f904f91b7abc43c683669602ea Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 7 Feb 2019 16:46:59 +0200 Subject: [PATCH 03/17] make DropDown support mouse wheel --- src/Avalonia.Controls/DropDown.cs | 53 ++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index 93b33e0589..684279a3cd 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -56,6 +56,7 @@ namespace Avalonia.Controls private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; + private IDisposable _subscriptionsOnOpen; /// /// Initializes static members of the class. @@ -174,6 +175,37 @@ namespace Avalonia.Controls } } + /// + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + base.OnPointerWheelChanged(e); + + if (!e.Handled) + { + if (!IsDropDownOpen) + { + if (IsFocused) + { + if (e.Delta.Y < 0) + { + if (++SelectedIndex >= ItemCount) + SelectedIndex = 0; + } + else + { + if (--SelectedIndex < 0) + SelectedIndex = ItemCount - 1; + } + e.Handled = true; + } + } + else + { + e.Handled = true; + } + } + } + /// protected override void OnPointerPressed(PointerPressedEventArgs e) { @@ -223,6 +255,9 @@ namespace Avalonia.Controls private void PopupClosed(object sender, EventArgs e) { + _subscriptionsOnOpen?.Dispose(); + _subscriptionsOnOpen = null; + if (CanFocus(this)) { Focus(); @@ -232,6 +267,22 @@ namespace Avalonia.Controls private void PopupOpened(object sender, EventArgs e) { TryFocusSelectedItem(); + + _subscriptionsOnOpen?.Dispose(); + _subscriptionsOnOpen = null; + + var toplevel = this.GetVisualRoot() as TopLevel; + if (toplevel != null) + { + _subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) => + { + //eat wheel scroll event outside dropdown popup while it's open + if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel) + { + ev.Handled = true; + } + }, Interactivity.RoutingStrategies.Tunnel); + } } private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e) @@ -247,7 +298,7 @@ namespace Avalonia.Controls { var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); - if(container == null && SelectedItems.Count > 0) + if (container == null && SelectedItems.Count > 0) { ScrollIntoView(SelectedItems[0]); container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); From f285c38d2766368767f2fee844743fd7365af821 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 7 Feb 2019 17:22:56 +0200 Subject: [PATCH 04/17] improve dropdown key up/down and mouse wheel auto selections so they don't go through unselected state --- src/Avalonia.Controls/DropDown.cs | 39 ++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index 684279a3cd..4f65a0aed4 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -150,16 +150,12 @@ namespace Avalonia.Controls { if (e.Key == Key.Down) { - if (++SelectedIndex >= ItemCount) - SelectedIndex = 0; - + SelectNext(); e.Handled = true; } else if (e.Key == Key.Up) { - if (--SelectedIndex < 0) - SelectedIndex = ItemCount - 1; - + SelectPrev(); e.Handled = true; } } @@ -187,15 +183,10 @@ namespace Avalonia.Controls if (IsFocused) { if (e.Delta.Y < 0) - { - if (++SelectedIndex >= ItemCount) - SelectedIndex = 0; - } + SelectNext(); else - { - if (--SelectedIndex < 0) - SelectedIndex = ItemCount - 1; - } + SelectPrev(); + e.Handled = true; } } @@ -358,5 +349,25 @@ namespace Avalonia.Controls } } } + + private void SelectNext() + { + int next = SelectedIndex + 1; + + if (next >= ItemCount) + next = 0; + + SelectedIndex = next; + } + + private void SelectPrev() + { + int prev = SelectedIndex - 1; + + if (prev < 0) + prev = ItemCount - 1; + + SelectedIndex = prev; + } } } From 990bc26d7b42c9437b128c0955cc79639a36f4d9 Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 10 Feb 2019 12:42:53 +0300 Subject: [PATCH 05/17] Add RoutedViewHost control implementation --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 157 ++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/Avalonia.ReactiveUI/RoutedViewHost.cs diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs new file mode 100644 index 0000000000..8006ef9aa6 --- /dev/null +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -0,0 +1,157 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Styling; +using ReactiveUI; +using Splat; + +namespace Avalonia +{ + /// + /// This control hosts the View associated with a Router, and will display + /// the View and wire up the ViewModel whenever a new ViewModel is navigated to. + /// + public class RoutedViewHost : UserControl, IActivatable, IEnableLogger + { + /// + /// The router dependency property. + /// + public static readonly AvaloniaProperty RouterProperty = + AvaloniaProperty.Register(nameof(Router)); + + /// + /// The default content property. + /// + public static readonly AvaloniaProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + + private readonly IAnimation _fadeOutAnimation = CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)); + private readonly IAnimation _fadeInAnimation = CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)); + + /// + /// Initializes a new instance of the class. + /// + public RoutedViewHost() + { + this.WhenActivated(disposables => + { + this.WhenAnyObservable(x => x.Router.CurrentViewModel) + .DistinctUntilChanged() + .Subscribe(HandleViewModelChange) + .DisposeWith(disposables); + }); + } + + /// + /// Gets or sets the ReactiveUI view locator used by this router. + /// + public IViewLocator ViewLocator { get; set; } + + /// + /// Gets or sets the of the view model stack. + /// + public RoutingState Router + { + get => GetValue(RouterProperty); + set => SetValue(RouterProperty, value); + } + + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Duplicates the Content property with a private setter. + /// + public new object Content + { + get => base.Content; + private set => base.Content = value; + } + + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + /// + /// Thrown when ViewLocator is unable to find the appropriate view. + /// + private void HandleViewModelChange(IRoutableViewModel viewModel) + { + if (viewModel == null) + { + this.Log().Info("ViewModel is null, falling back to default content."); + UpdateContent(DefaultContent); + return; + } + + var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; + var view = viewLocator.ResolveView(viewModel); + if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); + + this.Log().Info($"Ready to show {view} with autowired {viewModel}."); + view.ViewModel = viewModel; + UpdateContent(view); + } + + /// + /// Updates the content with transitions. + /// + /// New content to set. + private async void UpdateContent(object newContent) + { + await _fadeOutAnimation.RunAsync(this, null); + Content = newContent; + await _fadeInAnimation.RunAsync(this, null); + } + + /// + /// Creates opacity animation for this routed view host. + /// + /// Opacity to start from. + /// Opacity to finish with. + /// Duration of the animation. + /// Animation object instance. + private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) + { + return new Avalonia.Animation.Animation + { + Duration = duration, + Children = + { + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = from + } + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = to + } + }, + Cue = new Cue(1d) + } + } + }; + } + } +} From 6e4b9a23c248a33961a501ef614a753c4b9b805f Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 10 Feb 2019 16:28:22 +0300 Subject: [PATCH 06/17] Add unit tests for RoutedViewHost --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 59 +++++++--- .../RoutedViewHostTest.cs | 104 ++++++++++++++++++ 2 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 8006ef9aa6..3613d3d259 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -26,9 +26,20 @@ namespace Avalonia /// public static readonly AvaloniaProperty DefaultContentProperty = AvaloniaProperty.Register(nameof(DefaultContent)); - - private readonly IAnimation _fadeOutAnimation = CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)); - private readonly IAnimation _fadeInAnimation = CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)); + + /// + /// Fade in animation property. + /// + public static readonly AvaloniaProperty FadeInAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); + + /// + /// Fade out animation property. + /// + public static readonly AvaloniaProperty FadeOutAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); /// /// Initializes a new instance of the class. @@ -39,16 +50,11 @@ namespace Avalonia { this.WhenAnyObservable(x => x.Router.CurrentViewModel) .DistinctUntilChanged() - .Subscribe(HandleViewModelChange) + .Subscribe(NavigateToViewModel) .DisposeWith(disposables); }); } - /// - /// Gets or sets the ReactiveUI view locator used by this router. - /// - public IViewLocator ViewLocator { get; set; } - /// /// Gets or sets the of the view model stack. /// @@ -66,15 +72,38 @@ namespace Avalonia get => GetValue(DefaultContentProperty); set => SetValue(DefaultContentProperty, value); } + + /// + /// Gets or sets the animation played when page appears. + /// + public IAnimation FadeInAnimation + { + get => GetValue(FadeInAnimationProperty); + set => SetValue(FadeInAnimationProperty, value); + } + + /// + /// Gets or sets the animation played when page disappears. + /// + public IAnimation FadeOutAnimation + { + get => GetValue(FadeOutAnimationProperty); + set => SetValue(FadeOutAnimationProperty, value); + } /// /// Duplicates the Content property with a private setter. /// public new object Content { - get => base.Content; + get => base.Content ?? DefaultContent; private set => base.Content = value; } + + /// + /// Gets or sets the ReactiveUI view locator used by this router. + /// + public IViewLocator ViewLocator { get; set; } /// /// Invoked when ReactiveUI router navigates to a view model. @@ -83,12 +112,12 @@ namespace Avalonia /// /// Thrown when ViewLocator is unable to find the appropriate view. /// - private void HandleViewModelChange(IRoutableViewModel viewModel) + private void NavigateToViewModel(IRoutableViewModel viewModel) { if (viewModel == null) { this.Log().Info("ViewModel is null, falling back to default content."); - UpdateContent(DefaultContent); + UpdateContent(null); return; } @@ -107,9 +136,11 @@ namespace Avalonia /// New content to set. private async void UpdateContent(object newContent) { - await _fadeOutAnimation.RunAsync(this, null); + if (FadeOutAnimation != null) + await FadeOutAnimation.RunAsync(this, Clock); Content = newContent; - await _fadeInAnimation.RunAsync(this, null); + if (FadeInAnimation != null) + await FadeInAnimation.RunAsync(this, Clock); } /// diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs new file mode 100644 index 0000000000..c85b999af2 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -0,0 +1,104 @@ +using System; +using System.Reactive.Concurrency; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Rendering; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Avalonia; +using ReactiveUI; +using DynamicData; +using Xunit; +using Splat; +using Avalonia.Markup.Xaml; +using System.ComponentModel; +using System.Threading.Tasks; +using System.Reactive; + +namespace Avalonia +{ + public class RoutedViewHostTest + { + public class FirstRoutableViewModel : ReactiveObject, IRoutableViewModel + { + public string UrlPathSegment => "first"; + + public IScreen HostScreen { get; set; } + } + + public class FirstRoutableView : ReactiveUserControl { } + + public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel + { + public string UrlPathSegment => "second"; + + public IScreen HostScreen { get; set; } + } + + public class SecondRoutableView : ReactiveUserControl { } + + public class ScreenViewModel : ReactiveObject, IScreen + { + public RoutingState Router { get; } = new RoutingState(); + } + + public RoutedViewHostTest() + { + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor)); + } + + [Fact] + public void RoutedViewHostShouldStayInSyncWithRoutingState() + { + var screen = new ScreenViewModel(); + var defaultContent = new TextBlock(); + var host = new RoutedViewHost + { + Router = screen.Router, + DefaultContent = defaultContent, + FadeOutAnimation = null, + FadeInAnimation = null + }; + + var root = new TestRoot + { + Child = host + }; + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + screen.Router.Navigate + .Execute(new FirstRoutableViewModel()) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstRoutableView), host.Content.GetType()); + + screen.Router.Navigate + .Execute(new SecondRoutableViewModel()) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(SecondRoutableView), host.Content.GetType()); + + screen.Router.NavigateBack + .Execute(Unit.Default) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstRoutableView), host.Content.GetType()); + + screen.Router.NavigateBack + .Execute(Unit.Default) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + } + } +} \ No newline at end of file From 724ec9b8bb0ee923632e7e43d764a957c6f7543e Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 10 Feb 2019 18:21:51 +0300 Subject: [PATCH 07/17] Add xml docs and usage examples --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 44 ++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 3613d3d259..726e086d9c 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -10,9 +10,45 @@ using Splat; namespace Avalonia { /// - /// This control hosts the View associated with a Router, and will display - /// the View and wire up the ViewModel whenever a new ViewModel is navigated to. + /// This control hosts the View associated with ReactiveUI RoutingState, + /// and will display the View and wire up the ViewModel whenever a new + /// ViewModel is navigated to. Nested routing is also supported. /// + /// + /// + /// ReactiveUI routing consists of an IScreen that contains current + /// RoutingState, several IRoutableViewModels, and a platform-specific + /// XAML control called RoutedViewHost. + /// + /// + /// RoutingState manages the ViewModel navigation stack and allows + /// ViewModels to navigate to other ViewModels. IScreen is the root of + /// a navigation stack; despite the name, its views don't have to occupy + /// the whole screen. RoutedViewHost monitors an instance of RoutingState, + /// responding to any changes in the navigation stack by creating and + /// embedding the appropriate view. + /// + /// + /// Place this control to a view containing your ViewModel that implements + /// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property. + /// + /// + /// + /// + /// + /// + /// ]]> + /// + /// + /// + /// See + /// ReactiveUI routing documentation website for more info. + /// + /// public class RoutedViewHost : UserControl, IActivatable, IEnableLogger { /// @@ -96,7 +132,7 @@ namespace Avalonia /// public new object Content { - get => base.Content ?? DefaultContent; + get => base.Content; private set => base.Content = value; } @@ -117,7 +153,7 @@ namespace Avalonia if (viewModel == null) { this.Log().Info("ViewModel is null, falling back to default content."); - UpdateContent(null); + UpdateContent(DefaultContent); return; } From fa8d8c896dbb722bbebdaf484de232fdeca0fdeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Mon, 11 Feb 2019 22:44:09 +0000 Subject: [PATCH 08/17] Removed redundant bool comparisons. --- src/Avalonia.Base/Data/Core/BindingExpression.cs | 2 +- src/Avalonia.Controls/AutoCompleteBox.cs | 2 +- src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs | 2 +- src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs | 2 +- src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index c4ffa839e0..f1717bde3b 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -177,7 +177,7 @@ namespace Avalonia.Data.Core protected override void Subscribed(IObserver observer, bool first) { - if (!first && _value != null && _value.TryGetTarget(out var val) == true) + if (!first && _value != null && _value.TryGetTarget(out var val)) { observer.OnNext(val); } diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 1bc402bc2f..b054804c86 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1893,7 +1893,7 @@ namespace Avalonia.Controls { bool callTextChanged = false; // Update the Text dependency property - if ((userInitiated == null || userInitiated == true) && Text != value) + if ((userInitiated ?? true) && Text != value) { _ignoreTextPropertyChange++; Text = value; diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index c05c1672f8..30330ef9ac 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -285,7 +285,7 @@ namespace Avalonia.Controls.Presenters { scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable); - if (scrollable.IsLogicalScrollEnabled == true) + if (scrollable.IsLogicalScrollEnabled) { _logicalScrollSubscription = new CompositeDisposable( this.GetObservable(CanHorizontallyScrollProperty) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 159c3cd0fa..2fb8e84a2e 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -236,7 +236,7 @@ namespace Avalonia.Rendering.SceneGraph { foreach (var operation in DrawOperations) { - if (operation.Item.HitTest(p) == true) + if (operation.Item.HitTest(p)) { return true; } diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 255357e027..2720e674cc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -250,7 +250,7 @@ namespace Avalonia.Markup.Xaml .ToDictionary(entry =>entry.Element(arrayNs + "Key").Value, entry => entry.Element(arrayNs + "Value").Value); - if (xamlInfo.TryGetValue(typeName, out var rv) == true) + if (xamlInfo.TryGetValue(typeName, out var rv)) { yield return new Uri($"avares://{asm}{rv}"); yield break; From 91e678baf858054b39ee716c0a6a4d17f23f3bb3 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 12 Feb 2019 11:19:06 +0200 Subject: [PATCH 09/17] call canexecute before execute for button/menu command --- src/Avalonia.Controls/Button.cs | 2 +- src/Avalonia.Controls/MenuItem.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index d485924885..f572c67284 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -217,7 +217,7 @@ namespace Avalonia.Controls var e = new RoutedEventArgs(ClickEvent); RaiseEvent(e); - if (Command != null) + if (!e.Handled && Command?.CanExecute(CommandParameter) == true) { Command.Execute(CommandParameter); e.Handled = true; diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 055d49fb0b..aa20ee0595 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -287,7 +287,7 @@ namespace Avalonia.Controls /// The click event args. protected virtual void OnClick(RoutedEventArgs e) { - if (Command != null) + if (!e.Handled && Command?.CanExecute(CommandParameter) == true) { Command.Execute(CommandParameter); e.Handled = true; From 039991da684187c5d64104ca310a14a6793b496f Mon Sep 17 00:00:00 2001 From: mstr2 Date: Wed, 13 Feb 2019 03:54:04 +0100 Subject: [PATCH 10/17] Fixed a bug where AddOwner would add a property to AvaloniaPropertyRegistry's property list more than once --- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 12 ++++++++---- .../AvaloniaPropertyRegistryTests.cs | 5 +++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 6f57dfbf13..5fcdf76c0f 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -13,8 +13,8 @@ namespace Avalonia /// public class AvaloniaPropertyRegistry { - private readonly List _properties = - new List(); + private readonly Dictionary _properties = + new Dictionary(); private readonly Dictionary> _registered = new Dictionary>(); private readonly Dictionary> _attached = @@ -33,7 +33,7 @@ namespace Avalonia /// /// Gets a list of all registered properties. /// - internal IReadOnlyList Properties => _properties; + internal IReadOnlyCollection Properties => _properties.Values; /// /// Gets all non-attached s registered on a type. @@ -220,7 +220,11 @@ namespace Avalonia inner.Add(property.Id, property); } - _properties.Add(property); + if (!_properties.ContainsKey(property.Id)) + { + _properties.Add(property.Id, property); + } + _registeredCache.Clear(); } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs index 8220b7d6e7..d11319114f 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs @@ -27,6 +27,7 @@ namespace Avalonia.Base.UnitTests var property = new AttachedProperty("test", typeof(object), metadata, true); registry.Register(typeof(object), property); registry.RegisterAttached(typeof(AvaloniaPropertyRegistryTests), property); + property.AddOwner(); Assert.Equal(1, registry.Properties.Count); } @@ -150,5 +151,9 @@ namespace Avalonia.Base.UnitTests private class AttachedOwner2 : AttachedOwner { } + + private class Class4 : AvaloniaObject + { + } } } From 0fce33e43ea0c1dd3d2646ea3dda51acc821b676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Wed, 13 Feb 2019 23:37:39 +0000 Subject: [PATCH 11/17] XML comment fixes. --- src/Avalonia.Controls/MenuItem.cs | 2 +- src/Avalonia.Controls/PixelPointEventArgs.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 055d49fb0b..99e00ce72e 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -421,7 +421,7 @@ namespace Avalonia.Controls } /// - /// Called when the property changes. + /// Called when the property changes. /// /// The property change event. private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) diff --git a/src/Avalonia.Controls/PixelPointEventArgs.cs b/src/Avalonia.Controls/PixelPointEventArgs.cs index 55a3d5601f..2456d0aea4 100644 --- a/src/Avalonia.Controls/PixelPointEventArgs.cs +++ b/src/Avalonia.Controls/PixelPointEventArgs.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - /// The data. + /// The data. public PixelPointEventArgs(PixelPoint point) { Point = point; From 7b8b6374a056191feb2c90d916872107d80e359a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 Feb 2019 21:17:33 +0000 Subject: [PATCH 12/17] Scaling of 1 on monitors >= FullHD --- src/Avalonia.X11/X11Screens.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs index f2a0520c10..38f685ed0d 100644 --- a/src/Avalonia.X11/X11Screens.cs +++ b/src/Avalonia.X11/X11Screens.cs @@ -99,6 +99,8 @@ namespace Avalonia.X11 { if (mon.MWidth == 0) density = 1; + else if (mon.Width <= 1920) + density = 1; else density = X11Screen.GuessPixelDensity(mon.Width, mon.MWidth); } @@ -237,7 +239,14 @@ namespace Avalonia.X11 } else if (pixelDensity == null) { - PixelDensity = GuessPixelDensity(bounds.Width, physicalSize.Value.Width); + if (bounds.Width <= 1920) + { + PixelDensity = 1; + } + else + { + PixelDensity = GuessPixelDensity(bounds.Width, physicalSize.Value.Width); + } } else { From 131a4d90eff1a65d4484a55e1a10280d4dfa90d5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Feb 2019 11:09:34 +0100 Subject: [PATCH 13/17] Remove Avalonia.ISupportInitialize. This is a relic from when we were targeting a PCL profile that didn't have `System.ComponentModel.ISupportInitialize`. Now that we have that, use it instead. --- src/Avalonia.Base/ISupportInitialize.cs | 22 ------------ src/Avalonia.Controls/Control.cs | 1 + .../Embedding/EmbeddableControlRoot.cs | 1 + .../Embedding/Offscreen/OffscreenTopLevel.cs | 1 + src/Avalonia.Controls/WindowBase.cs | 1 + .../AvaloniaXamlLoader.cs | 1 + .../PortableXaml/AvaloniaXamlObjectWriter.cs | 35 ------------------- .../Primitives/SelectingItemsControlTests.cs | 1 + .../Xaml/InitializationOrderTracker.cs | 3 +- .../StyledElementTests.cs | 1 + 10 files changed, 9 insertions(+), 58 deletions(-) delete mode 100644 src/Avalonia.Base/ISupportInitialize.cs diff --git a/src/Avalonia.Base/ISupportInitialize.cs b/src/Avalonia.Base/ISupportInitialize.cs deleted file mode 100644 index 04e3d72e6c..0000000000 --- a/src/Avalonia.Base/ISupportInitialize.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -namespace Avalonia -{ - /// - /// Specifies that this object supports a simple, transacted notification for batch - /// initialization. - /// - public interface ISupportInitialize - { - /// - /// Signals the object that initialization is starting. - /// - void BeginInit(); - - /// - /// Signals the object that initialization is complete. - /// - void EndInit(); - } -} diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index a00d586233..a7ee027e70 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.ComponentModel; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 224af979ab..43beb923e5 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Platform; diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index 8b39cc03b8..c4f83ffd54 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Styling; namespace Avalonia.Controls.Embedding.Offscreen diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 56ffd315f1..363af05a0b 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 2720e674cc..b99864b050 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -8,6 +8,7 @@ using Avalonia.Platform; using Portable.Xaml; using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Reflection; using System.Runtime.Serialization; diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs index 5d1a98f6f8..9fa6c26c35 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs @@ -77,40 +77,15 @@ namespace Avalonia.Markup.Xaml.PortableXaml _delayedValuesHelper.ApplyAll(); } - protected internal override void OnAfterBeginInit(object value) - { - //not called for avalonia objects - //as it's called inly for - //Portable.Xaml.ComponentModel.ISupportInitialize - base.OnAfterBeginInit(value); - } - - protected internal override void OnAfterEndInit(object value) - { - //not called for avalonia objects - //as it's called inly for - //Portable.Xaml.ComponentModel.ISupportInitialize - base.OnAfterEndInit(value); - } - protected internal override void OnAfterProperties(object value) { _delayedValuesHelper.EndInit(value); base.OnAfterProperties(value); - - //AfterEndInit is not called as it supports only - //Portable.Xaml.ComponentModel.ISupportInitialize - //and we have Avalonia.ISupportInitialize so we need some hacks - HandleEndEdit(value); } protected internal override void OnBeforeProperties(object value) { - //OnAfterBeginInit is not called as it supports only - //Portable.Xaml.ComponentModel.ISupportInitialize - //and we have Avalonia.ISupportInitialize so we need some hacks - HandleBeginInit(value); if (value != null) _delayedValuesHelper.BeginInit(value); @@ -127,16 +102,6 @@ namespace Avalonia.Markup.Xaml.PortableXaml return base.OnSetValue(target, member, value); } - private void HandleBeginInit(object value) - { - (value as Avalonia.ISupportInitialize)?.BeginInit(); - } - - private void HandleEndEdit(object value) - { - (value as Avalonia.ISupportInitialize)?.EndInit(); - } - public override void WriteStartMember(XamlMember property) { foreach(var d in DesignDirectives) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index bbe1d85acb..2df925301f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs index 3ecb2d9f37..104f46cbac 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs @@ -4,6 +4,7 @@ using Avalonia.Controls; using Avalonia.LogicalTree; using System.Collections.Generic; +using System.ComponentModel; namespace Avalonia.Markup.Xaml.UnitTests.Xaml { @@ -39,4 +40,4 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Order.Add($"EndInit {InitState}"); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs index 4096dcf380..4970addd81 100644 --- a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs @@ -10,6 +10,7 @@ using Avalonia.UnitTests; using Xunit; using Avalonia.LogicalTree; using Avalonia.Controls; +using System.ComponentModel; namespace Avalonia.Styling.UnitTests { From 27565d80bde42495edd9f1861ed3cc2fba50b941 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 15 Feb 2019 10:24:25 +0000 Subject: [PATCH 14/17] [X11] put FullHd res check inside GuessPixelDensity. --- src/Avalonia.X11/X11Screens.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs index 38f685ed0d..ad5cad7eae 100644 --- a/src/Avalonia.X11/X11Screens.cs +++ b/src/Avalonia.X11/X11Screens.cs @@ -11,6 +11,7 @@ namespace Avalonia.X11 { class X11Screens : IScreenImpl { + private const int FullHDWidth = 1920; private IX11Screens _impl; public X11Screens(IX11Screens impl) @@ -99,8 +100,6 @@ namespace Avalonia.X11 { if (mon.MWidth == 0) density = 1; - else if (mon.Width <= 1920) - density = 1; else density = X11Screen.GuessPixelDensity(mon.Width, mon.MWidth); } @@ -239,14 +238,7 @@ namespace Avalonia.X11 } else if (pixelDensity == null) { - if (bounds.Width <= 1920) - { - PixelDensity = 1; - } - else - { - PixelDensity = GuessPixelDensity(bounds.Width, physicalSize.Value.Width); - } + PixelDensity = GuessPixelDensity(bounds.Width, physicalSize.Value.Width); } else { @@ -256,6 +248,6 @@ namespace Avalonia.X11 } public static double GuessPixelDensity(double pixelWidth, double mmWidth) - => Math.Max(1, Math.Round(pixelWidth / mmWidth * 25.4 / 96)); + => pixelWidth <= FullHDWidth ? 1 : Math.Max(1, Math.Round(pixelWidth / mmWidth * 25.4 / 96)); } } From 1a661e657f270a33a6152e56789fa4b2296b76fe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 15 Feb 2019 11:14:07 +0000 Subject: [PATCH 15/17] fix error. --- src/Avalonia.X11/X11Screens.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs index ad5cad7eae..6bfc8779da 100644 --- a/src/Avalonia.X11/X11Screens.cs +++ b/src/Avalonia.X11/X11Screens.cs @@ -11,7 +11,6 @@ namespace Avalonia.X11 { class X11Screens : IScreenImpl { - private const int FullHDWidth = 1920; private IX11Screens _impl; public X11Screens(IX11Screens impl) @@ -219,6 +218,7 @@ namespace Avalonia.X11 class X11Screen { + private const int FullHDWidth = 1920; public bool Primary { get; } public string Name { get; set; } public PixelRect Bounds { get; set; } From a88c1473da0f05a409bb9c7160ff1a1800791702 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 18 Feb 2019 00:16:02 +0000 Subject: [PATCH 16/17] [OSX/Avalonia.Native] fix NRE in double dispose of ScreenImpl --- src/Avalonia.Native/ScreenImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/ScreenImpl.cs b/src/Avalonia.Native/ScreenImpl.cs index c1edd6c846..0729de9b8e 100644 --- a/src/Avalonia.Native/ScreenImpl.cs +++ b/src/Avalonia.Native/ScreenImpl.cs @@ -41,7 +41,7 @@ namespace Avalonia.Native public void Dispose () { - _native.Dispose(); + _native?.Dispose(); _native = null; } } From 71cee6ae5bb3442da56fe0c0cc3dfc19f5a1a589 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 19 Feb 2019 11:11:42 +0100 Subject: [PATCH 17/17] Fix ItemTemplateProperty being subscribed to on every ItemsControl or derived class construction. --- src/Avalonia.Controls/ItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index d74078c712..3dfeae52a4 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -64,6 +64,7 @@ namespace Avalonia.Controls static ItemsControl() { ItemsProperty.Changed.AddClassHandler(x => x.ItemsChanged); + ItemTemplateProperty.Changed.AddClassHandler(x => x.ItemTemplateChanged); } /// @@ -73,7 +74,6 @@ namespace Avalonia.Controls { PseudoClasses.Add(":empty"); SubscribeToItems(_items); - ItemTemplateProperty.Changed.AddClassHandler(x => x.ItemTemplateChanged); } ///