From 370590f3e02964d64b6b35a3e10bf94289b7409e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 18 Mar 2020 11:15:01 +0100 Subject: [PATCH 01/17] Add failing tests for #2838. --- .../Xaml/StyleTests.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index f7629e5b9e..818aa29f1c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -278,5 +278,67 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Colors.Red, ((ISolidColorBrush)notFoo.Background).Color); } } + + [Fact] + public void Style_Can_Use_Or_Selector_1() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var foo = window.FindControl("foo"); + var bar = window.FindControl("bar"); + var baz = window.FindControl("baz"); + + Assert.Equal(Brushes.Red, foo.Background); + Assert.Equal(Brushes.Red, bar.Background); + Assert.Null(baz.Background); + } + } + + [Fact] + public void Style_Can_Use_Or_Selector_2() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var border = window.FindControl("border"); + var canvas = window.FindControl("canvas"); + var listBox = window.FindControl("listBox"); + + Assert.Equal(Brushes.Red, border.Background); + Assert.Equal(Brushes.Red, canvas.Background); + Assert.Equal(Brushes.Red, listBox.Background); + } + } } } From 33d3a66c8621f60c8d6e2fb6bab03e00a253bf86 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 18 Mar 2020 11:21:40 +0100 Subject: [PATCH 02/17] Fix comma (or) style selector. - `Selector.Or`'s first parameter isn't a `Selector` - remove that check as I don't think it's needed anyway - Make sure last selector is added to `XamlIlOrSelectorNode` Fixes #2838 --- .../Transformers/AvaloniaXamlIlSelectorTransformer.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index aac07f5b6e..96d78b5092 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -104,6 +104,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } } + if (results != null && result != null) + { + results.Add(result); + } + return results ?? result; } @@ -158,9 +163,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers protected void EmitCall(XamlIlEmitContext context, IXamlIlEmitter codeGen, Func method) { var selectors = context.Configuration.TypeSystem.GetType("Avalonia.Styling.Selectors"); - var found = selectors.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && - m.Parameters[0].FullName == "Avalonia.Styling.Selector" - && method(m)); + var found = selectors.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && method(m)); codeGen.EmitCall(found); } } From 7f0c502c296e5936385a36dd99e3e97ad0cb5ea4 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 23 Mar 2020 12:51:26 +0200 Subject: [PATCH 03/17] add simple unit test for multibinding --- .../AvaloniaObjectTests_MultiBinding.cs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs new file mode 100644 index 0000000000..30693e2cb3 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs @@ -0,0 +1,59 @@ +// 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.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_MultiBinding + { + [Fact] + public void Should_Update() + { + var target = new Class1(); + + var b = new Subject(); + + var mb = new MultiBinding() + { + Converter = StringJoinConverter, + Bindings = new[] + { + b.ToBinding() + } + }; + target.Bind(Class1.FooProperty, mb); + + Assert.Equal(null, target.Foo); + + b.OnNext("Foo"); + + Assert.Equal("Foo", target.Foo); + + b.OnNext("Bar"); + + Assert.Equal("Bar", target.Foo); + } + + private static IMultiValueConverter StringJoinConverter = new FuncMultiValueConverter(v => string.Join(",", v.ToArray())); + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register("Foo"); + + public string Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + } + } +} From 613ba3ffd2e207220aa56ab4bd2006210a63119c Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 23 Mar 2020 12:52:09 +0200 Subject: [PATCH 04/17] add another test for multibinding --- .../AvaloniaObjectTests_MultiBinding.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs index 30693e2cb3..19ffb014d4 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs @@ -42,6 +42,35 @@ namespace Avalonia.Base.UnitTests Assert.Equal("Bar", target.Foo); } + [Fact] + public void Should_Update_With_Multiple_Bindings() + { + var target = new Class1(); + + var bindings = Enumerable.Range(0, 3).Select(i => new BehaviorSubject("Empty")).ToArray(); + + var mb = new MultiBinding() + { + Converter = StringJoinConverter, + Bindings = bindings.Select(b => b.ToBinding()).ToArray() + }; + target.Bind(Class1.FooProperty, mb); + + Assert.Equal("Empty,Empty,Empty", target.Foo); + + bindings[0].OnNext("Foo"); + + Assert.Equal("Foo,Empty,Empty", target.Foo); + + bindings[1].OnNext("Bar"); + + Assert.Equal("Foo,Bar,Empty", target.Foo); + + bindings[2].OnNext("Baz"); + + Assert.Equal("Foo,Bar,Baz", target.Foo); + } + private static IMultiValueConverter StringJoinConverter = new FuncMultiValueConverter(v => string.Join(",", v.ToArray())); private class Class1 : AvaloniaObject From 50deb04e7ab060e42c078a51c30e5b529019dbb9 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 23 Mar 2020 12:56:25 +0200 Subject: [PATCH 05/17] add failing unit test for #3692 --- .../AvaloniaObjectTests_MultiBinding.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs index 19ffb014d4..8560966fb7 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs @@ -71,6 +71,34 @@ namespace Avalonia.Base.UnitTests Assert.Equal("Foo,Bar,Baz", target.Foo); } + [Fact] + public void Should_Update_When_Null_Value_In_Bindings() + { + var target = new Class1(); + + var b = new Subject(); + + var mb = new MultiBinding() + { + Converter = StringJoinConverter, + Bindings = new[] + { + b.ToBinding() + } + }; + target.Bind(Class1.FooProperty, mb); + + Assert.Equal(null, target.Foo); + + b.OnNext("Foo"); + + Assert.Equal("Foo", target.Foo); + + b.OnNext(null); + + Assert.Equal("", target.Foo); + } + private static IMultiValueConverter StringJoinConverter = new FuncMultiValueConverter(v => string.Join(",", v.ToArray())); private class Class1 : AvaloniaObject From ec92b90e81601a040fd9d25d0b70d5aebeb49e7c Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 23 Mar 2020 12:57:27 +0200 Subject: [PATCH 06/17] add a test for multibinding with StringFormat --- .../AvaloniaObjectTests_MultiBinding.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs index 8560966fb7..ab413dd550 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs @@ -99,6 +99,30 @@ namespace Avalonia.Base.UnitTests Assert.Equal("", target.Foo); } + [Fact] + public void Should_Update_When_Null_Value_In_Bindings_With_StringFormat() + { + var target = new Class1(); + + var b = new Subject(); + + var mb = new MultiBinding() + { + StringFormat = "Converted: {0}", + Bindings = new[] + { + b.ToBinding() + } + }; + target.Bind(Class1.FooProperty, mb); + + Assert.Equal(null, target.Foo); + b.OnNext("Foo"); + Assert.Equal("Converted: Foo", target.Foo); + b.OnNext(null); + Assert.Equal("Converted: ", target.Foo); + } + private static IMultiValueConverter StringJoinConverter = new FuncMultiValueConverter(v => string.Join(",", v.ToArray())); private class Class1 : AvaloniaObject From 3dc5b8bda19f1031e95760f4964098e4b01c5f64 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 23 Mar 2020 12:59:30 +0200 Subject: [PATCH 07/17] add failing test for the core reason of the issue #3692 --- .../AvaloniaObjectTests_MultiBinding.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs index ab413dd550..757160a0a4 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs @@ -123,6 +123,44 @@ namespace Avalonia.Base.UnitTests Assert.Equal("Converted: ", target.Foo); } + [Fact] + public void MultiValueConverter_Should_Not_Skip_Valid_Null_ReferenceType_Value() + { + var target = new FuncMultiValueConverter(v => string.Join(",", v.ToArray())); + + object value = target.Convert(new[] { "Foo", "Bar", "Baz" }, typeof(string), null, CultureInfo.InvariantCulture); + + Assert.Equal("Foo,Bar,Baz", value); + + value = target.Convert(new[] { null, "Bar", "Baz" }, typeof(string), null, CultureInfo.InvariantCulture); + + Assert.Equal(",Bar,Baz", value); + } + + [Fact] + public void MultiValueConverter_Should_Not_Skip_Valid_Default_ValueType_Value() + { + var target = new FuncMultiValueConverter(v => string.Join(",", v.ToArray())); + + IList create(string[] values) => + values.Select(v => (object)(v != null ? new StringValueTypeWrapper() { Value = v } : default)).ToList(); + + object value = target.Convert(create(new[] { "Foo", "Bar", "Baz" }), typeof(string), null, CultureInfo.InvariantCulture); + + Assert.Equal("Foo,Bar,Baz", value); + + value = target.Convert(create(new[] { null, "Bar", "Baz" }), typeof(string), null, CultureInfo.InvariantCulture); + + Assert.Equal(",Bar,Baz", value); + } + + private struct StringValueTypeWrapper + { + public string Value; + + public override string ToString() => Value; + } + private static IMultiValueConverter StringJoinConverter = new FuncMultiValueConverter(v => string.Join(",", v.ToArray())); private class Class1 : AvaloniaObject From 6e54db32e94d1166fa74b0391eafe1ddd6a49d9b Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 23 Mar 2020 13:22:58 +0200 Subject: [PATCH 08/17] fix #3692 --- .../Data/Converters/FuncMultiValueConverter.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs b/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs index 6e1c4cb0e3..887641b5c3 100644 --- a/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs @@ -30,7 +30,23 @@ namespace Avalonia.Data.Converters /// public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) { - var converted = values.OfType().ToList(); + //standard OfType skip null values, even they are valid for the Type + static IEnumerable OfTypeWithDefaultSupport(IList list) + { + foreach (object obj in list) + { + if (obj is TIn result) + { + yield return result; + } + else if (Equals(obj, default(TIn))) + { + yield return default(TIn); + } + } + } + + var converted = OfTypeWithDefaultSupport(values).ToList(); if (converted.Count == values.Count) { From b9313c2dec5cd00c7fe427a9946ffce96d5f3ee8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Apr 2020 11:28:09 +0200 Subject: [PATCH 09/17] Fix large/small scroll in ScrollViewer. For non-logical scrolling: - Use 16 for small scroll size (value taken from WPF) - Use viewport size for large scroll For logical scrolling, use the `ScrollSize`/`PageScrollSize` defined on `ILogicalScrollable`. Note that this required a small breaking change to `ILogicalScrollable`. Fixed #3245 --- .../Presenters/ItemVirtualizer.cs | 41 +++++-- .../Presenters/ItemsPresenter.cs | 21 +++- .../Presenters/ScrollContentPresenter.cs | 11 +- .../Primitives/ILogicalScrollable.cs | 31 +++-- src/Avalonia.Controls/ScrollViewer.cs | 113 +++++++++++++++--- src/Avalonia.Themes.Default/ScrollViewer.xaml | 4 + ...ontentPresenterTests_ILogicalScrollable.cs | 31 +++-- .../ScrollViewerTests.cs | 61 ++++++++++ 8 files changed, 255 insertions(+), 58 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 0b21a1f8b9..5ba6b6b9a3 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters { @@ -99,9 +100,14 @@ namespace Avalonia.Controls.Presenters { get { - return Vertical ? - new Size(Owner.Panel.DesiredSize.Width, ExtentValue) : - new Size(ExtentValue, Owner.Panel.DesiredSize.Height); + if (IsLogicalScrollEnabled) + { + return Vertical ? + new Size(Owner.Panel.DesiredSize.Width, ExtentValue) : + new Size(ExtentValue, Owner.Panel.DesiredSize.Height); + } + + return default; } } @@ -112,9 +118,14 @@ namespace Avalonia.Controls.Presenters { get { - return Vertical ? - new Size(Owner.Panel.Bounds.Width, ViewportValue) : - new Size(ViewportValue, Owner.Panel.Bounds.Height); + if (IsLogicalScrollEnabled) + { + return Vertical ? + new Size(Owner.Panel.Bounds.Width, ViewportValue) : + new Size(ViewportValue, Owner.Panel.Bounds.Height); + } + + return default; } } @@ -125,11 +136,21 @@ namespace Avalonia.Controls.Presenters { get { - return Vertical ? new Vector(_crossAxisOffset, OffsetValue) : new Vector(OffsetValue, _crossAxisOffset); + if (IsLogicalScrollEnabled) + { + return Vertical ? new Vector(_crossAxisOffset, OffsetValue) : new Vector(OffsetValue, _crossAxisOffset); + } + + return default; } set { + if (!IsLogicalScrollEnabled) + { + throw new NotSupportedException("Logical scrolling disabled."); + } + var oldCrossAxisOffset = _crossAxisOffset; if (Vertical) @@ -164,10 +185,10 @@ namespace Avalonia.Controls.Presenters } var virtualizingPanel = owner.Panel as IVirtualizingPanel; - var scrollable = (ILogicalScrollable)owner; + var scrollContentPresenter = owner.Parent as IScrollable; ItemVirtualizer result = null; - if (virtualizingPanel != null && scrollable.InvalidateScroll != null) + if (virtualizingPanel != null && scrollContentPresenter is object) { switch (owner.VirtualizationMode) { @@ -277,6 +298,6 @@ namespace Avalonia.Controls.Presenters /// /// Invalidates the current scroll. /// - protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll?.Invoke(); + protected void InvalidateScroll() => ((ILogicalScrollable)Owner).RaiseScrollInvalidated(EventArgs.Empty); } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 5943309cbb..02f2e7c9d9 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -21,6 +21,7 @@ namespace Avalonia.Controls.Presenters private bool _canHorizontallyScroll; private bool _canVerticallyScroll; + EventHandler _scrollInvalidated; /// /// Initializes static members of the class. @@ -95,13 +96,17 @@ namespace Avalonia.Controls.Presenters Size IScrollable.Viewport => Virtualizer?.Viewport ?? Bounds.Size; /// - Action ILogicalScrollable.InvalidateScroll { get; set; } + event EventHandler ILogicalScrollable.ScrollInvalidated + { + add => _scrollInvalidated += value; + remove => _scrollInvalidated -= value; + } /// - Size ILogicalScrollable.ScrollSize => new Size(1, 1); + Size ILogicalScrollable.ScrollSize => new Size(16, 1); /// - Size ILogicalScrollable.PageScrollSize => new Size(0, 1); + Size ILogicalScrollable.PageScrollSize => Virtualizer?.Viewport ?? new Size(16, 16); internal ItemVirtualizer Virtualizer { get; private set; } @@ -117,6 +122,12 @@ namespace Avalonia.Controls.Presenters return Virtualizer?.GetControlInDirection(direction, from); } + /// + void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e) + { + _scrollInvalidated?.Invoke(this, e); + } + public override void ScrollIntoView(object item) { Virtualizer?.ScrollIntoView(item); @@ -138,7 +149,7 @@ namespace Avalonia.Controls.Presenters { Virtualizer?.Dispose(); Virtualizer = ItemVirtualizer.Create(this); - ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); + _scrollInvalidated?.Invoke(this, EventArgs.Empty); KeyboardNavigation.SetTabNavigation( (InputElement)Panel, @@ -162,7 +173,7 @@ namespace Avalonia.Controls.Presenters { Virtualizer?.Dispose(); Virtualizer = ItemVirtualizer.Create(this); - ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); + _scrollInvalidated?.Invoke(this, EventArgs.Empty); } } } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index c0391c89ca..98a5b10023 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Runtime.InteropServices.ComTypes; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.LogicalTree; using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters @@ -349,7 +351,7 @@ namespace Avalonia.Controls.Presenters if (scrollable != null) { - scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable); + scrollable.ScrollInvalidated += ScrollInvalidated; if (scrollable.IsLogicalScrollEnabled) { @@ -360,12 +362,17 @@ namespace Avalonia.Controls.Presenters .Subscribe(x => scrollable.CanVerticallyScroll = x), this.GetObservable(OffsetProperty) .Skip(1).Subscribe(x => scrollable.Offset = x), - Disposable.Create(() => scrollable.InvalidateScroll = null)); + Disposable.Create(() => scrollable.ScrollInvalidated -= ScrollInvalidated)); UpdateFromScrollable(scrollable); } } } + private void ScrollInvalidated(object sender, EventArgs e) + { + UpdateFromScrollable((ILogicalScrollable)sender); + } + private void UpdateFromScrollable(ILogicalScrollable scrollable) { var logicalScroll = _logicalScrollSubscription != null; diff --git a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs index 6e8d097dd1..5c29945735 100644 --- a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs +++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs @@ -31,22 +31,6 @@ namespace Avalonia.Controls.Primitives /// bool IsLogicalScrollEnabled { get; } - /// - /// Gets or sets the scroll invalidation method. - /// - /// - /// - /// This method notifies the attached of a change in - /// the , or - /// properties. - /// - /// - /// This property is set by the parent when the - /// is placed inside it. - /// - /// - Action InvalidateScroll { get; set; } - /// /// Gets the size to scroll by, in logical units. /// @@ -57,6 +41,15 @@ namespace Avalonia.Controls.Primitives /// Size PageScrollSize { get; } + /// + /// Raised when the scroll is invalidated. + /// + /// + /// This event notifies an attached of a change in + /// one of the scroll properties. + /// + event EventHandler ScrollInvalidated; + /// /// Attempts to bring a portion of the target visual into view by scrolling the content. /// @@ -72,5 +65,11 @@ namespace Avalonia.Controls.Primitives /// The control from which movement begins. /// The control. IControl GetControlInDirection(NavigationDirection direction, IControl from); + + /// + /// Raises the event. + /// + /// The event args. + void RaiseScrollInvalidated(EventArgs e); } } diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index b1f340bd65..f5881a8efe 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -10,6 +10,8 @@ namespace Avalonia.Controls /// public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider { + private static readonly Size s_defaultSmallChange = new Size(16, 16); + /// /// Defines the property. /// @@ -59,6 +61,22 @@ namespace Avalonia.Controls o => o.Viewport, (o, v) => o.Viewport = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty LargeChangeProperty = + AvaloniaProperty.RegisterDirect( + nameof(LargeChange), + o => o.LargeChange); + + /// + /// Defines the property. + /// + public static readonly DirectProperty SmallChangeProperty = + AvaloniaProperty.RegisterDirect( + nameof(SmallChange), + o => o.SmallChange); + /// /// Defines the HorizontalScrollBarMaximum property. /// @@ -149,9 +167,13 @@ namespace Avalonia.Controls nameof(VerticalScrollBarVisibility), ScrollBarVisibility.Auto); + private IDisposable _childSubscription; + private ILogicalScrollable _logicalScrollable; private Size _extent; private Vector _offset; private Size _viewport; + private Size _largeChange; + private Size _smallChange = s_defaultSmallChange; /// /// Initializes static members of the class. @@ -228,6 +250,16 @@ namespace Avalonia.Controls } } + /// + /// Gets the large (page) change value for the scroll viewer. + /// + public Size LargeChange => _largeChange; + + /// + /// Gets the small (line) change value for the scroll viewer. + /// + public Size SmallChange => _smallChange; + /// /// Gets or sets the horizontal scrollbar visibility. /// @@ -246,22 +278,6 @@ namespace Avalonia.Controls set { SetValue(VerticalScrollBarVisibilityProperty, value); } } - /// - /// Scrolls to the top-left corner of the content. - /// - public void ScrollToHome() - { - Offset = new Vector(double.NegativeInfinity, double.NegativeInfinity); - } - - /// - /// Scrolls to the bottom-left corner of the content. - /// - public void ScrollToEnd() - { - Offset = new Vector(double.NegativeInfinity, double.PositiveInfinity); - } - /// /// Gets a value indicating whether the viewer can scroll horizontally. /// @@ -347,6 +363,22 @@ namespace Avalonia.Controls /// IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement + /// + /// Scrolls to the top-left corner of the content. + /// + public void ScrollToHome() + { + Offset = new Vector(double.NegativeInfinity, double.NegativeInfinity); + } + + /// + /// Scrolls to the bottom-left corner of the content. + /// + public void ScrollToEnd() + { + Offset = new Vector(double.NegativeInfinity, double.PositiveInfinity); + } + /// /// Gets the value of the HorizontalScrollBarVisibility attached property. /// @@ -397,6 +429,22 @@ namespace Avalonia.Controls // TODO: Implement } + protected override bool RegisterContentPresenter(IContentPresenter presenter) + { + _childSubscription?.Dispose(); + _childSubscription = null; + + if (base.RegisterContentPresenter(presenter)) + { + _childSubscription = Presenter? + .GetObservable(ContentPresenter.ChildProperty) + .Subscribe(ChildChanged); + return true; + } + + return false; + } + internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset) { var maxX = Math.Max(extent.Width - viewport.Width, 0); @@ -431,6 +479,28 @@ namespace Avalonia.Controls } } + private void ChildChanged(IControl child) + { + if (_logicalScrollable is object) + { + _logicalScrollable.ScrollInvalidated -= LogicalScrollInvalidated; + _logicalScrollable = null; + } + + if (child is ILogicalScrollable logical) + { + _logicalScrollable = logical; + logical.ScrollInvalidated += LogicalScrollInvalidated; + } + + CalculatedPropertiesChanged(); + } + + private void LogicalScrollInvalidated(object sender, EventArgs e) + { + CalculatedPropertiesChanged(); + } + private void ScrollBarVisibilityChanged(AvaloniaPropertyChangedEventArgs e) { var wasEnabled = !ScrollBarVisibility.Disabled.Equals(e.OldValue); @@ -465,6 +535,17 @@ namespace Avalonia.Controls RaisePropertyChanged(VerticalScrollBarMaximumProperty, 0, VerticalScrollBarMaximum); RaisePropertyChanged(VerticalScrollBarValueProperty, 0, VerticalScrollBarValue); RaisePropertyChanged(VerticalScrollBarViewportSizeProperty, 0, VerticalScrollBarViewportSize); + + if (_logicalScrollable?.IsLogicalScrollEnabled == true) + { + SetAndRaise(SmallChangeProperty, ref _smallChange, _logicalScrollable.ScrollSize); + SetAndRaise(LargeChangeProperty, ref _largeChange, _logicalScrollable.PageScrollSize); + } + else + { + SetAndRaise(SmallChangeProperty, ref _smallChange, s_defaultSmallChange); + SetAndRaise(LargeChangeProperty, ref _largeChange, Viewport); + } } protected override void OnKeyDown(KeyEventArgs e) diff --git a/src/Avalonia.Themes.Default/ScrollViewer.xaml b/src/Avalonia.Themes.Default/ScrollViewer.xaml index 38f4eef964..1d893133e1 100644 --- a/src/Avalonia.Themes.Default/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Default/ScrollViewer.xaml @@ -22,6 +22,8 @@ _scrollInvalidated != null; + + public event EventHandler ScrollInvalidated + { + add => _scrollInvalidated += value; + remove => _scrollInvalidated -= value; + } public Size Extent { @@ -331,7 +339,7 @@ namespace Avalonia.Controls.UnitTests set { _extent = value; - InvalidateScroll?.Invoke(); + _scrollInvalidated?.Invoke(this, EventArgs.Empty); } } @@ -341,7 +349,7 @@ namespace Avalonia.Controls.UnitTests set { _offset = value; - InvalidateScroll?.Invoke(); + _scrollInvalidated?.Invoke(this, EventArgs.Empty); } } @@ -351,7 +359,7 @@ namespace Avalonia.Controls.UnitTests set { _viewport = value; - InvalidateScroll?.Invoke(); + _scrollInvalidated?.Invoke(this, EventArgs.Empty); } } @@ -376,6 +384,11 @@ namespace Avalonia.Controls.UnitTests throw new NotImplementedException(); } + public void RaiseScrollInvalidated(EventArgs e) + { + _scrollInvalidated?.Invoke(this, e); + } + protected override Size MeasureOverride(Size availableSize) { AvailableSize = availableSize; @@ -388,4 +401,4 @@ namespace Avalonia.Controls.UnitTests } } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index f58a4cbffe..5375a244c9 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -4,6 +4,8 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Layout; +using Avalonia.LogicalTree; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests @@ -86,6 +88,65 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Vector(0, 40), target.Offset); } + [Fact] + public void SmallChange_Should_Be_16() + { + var target = new ScrollViewer(); + + Assert.Equal(new Size(16, 16), target.SmallChange); + } + + [Fact] + public void LargeChange_Should_Be_Viewport() + { + var target = new ScrollViewer(); + + target.SetValue(ScrollViewer.ViewportProperty, new Size(104, 143)); + Assert.Equal(new Size(104, 143), target.LargeChange); + } + + [Fact] + public void SmallChange_Should_Come_From_ILogicalScrollable_If_Present() + { + var child = new Mock(); + var logicalScroll = child.As(); + + logicalScroll.Setup(x => x.IsLogicalScrollEnabled).Returns(true); + logicalScroll.Setup(x => x.ScrollSize).Returns(new Size(12, 43)); + + var target = new ScrollViewer + { + Template = new FuncControlTemplate(CreateTemplate), + Content = child.Object, + }; + + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + Assert.Equal(new Size(12, 43), target.SmallChange); + } + + [Fact] + public void LargeChange_Should_Come_From_ILogicalScrollable_If_Present() + { + var child = new Mock(); + var logicalScroll = child.As(); + + logicalScroll.Setup(x => x.IsLogicalScrollEnabled).Returns(true); + logicalScroll.Setup(x => x.PageScrollSize).Returns(new Size(45, 67)); + + var target = new ScrollViewer + { + Template = new FuncControlTemplate(CreateTemplate), + Content = child.Object, + }; + + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + Assert.Equal(new Size(45, 67), target.LargeChange); + } + private Control CreateTemplate(ScrollViewer control, INameScope scope) { return new Grid From 3c74b9593e2534dabd2f4751c80276897fbb14ad Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Apr 2020 11:28:53 +0200 Subject: [PATCH 10/17] Auto-update to .sln file by VS 16.5. --- Avalonia.sln | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index 810ba5c12f..38f8e5f720 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -204,16 +204,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Ava EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 - src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 4 + src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 5 src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4 - src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 4 - tests\Avalonia.RenderTests\Avalonia.RenderTests.projitems*{dabfd304-d6a4-4752-8123-c2ccf7ac7831}*SharedItemsImports = 4 + src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 5 + src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 5 src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution From bc36330bc01615bcfc99736ee6d5a2ed1affaef3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 5 Apr 2020 17:19:37 +0200 Subject: [PATCH 11/17] Added failing ContextMenu test. Due to NRE introduced in #3745. --- .../ContextMenuTests.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index f44f89e91f..5a47a86e51 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -1,9 +1,5 @@ using System; -using System.Windows.Input; -using Avalonia.Controls.Primitives; -using Avalonia.Data; using Avalonia.Input; -using Avalonia.Markup.Data; using Avalonia.Platform; using Avalonia.UnitTests; using Moq; @@ -159,6 +155,19 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Can_Set_Clear_ContextMenu_Property() + { + using (Application()) + { + var target = new ContextMenu(); + var control = new Panel(); + + control.ContextMenu = target; + control.ContextMenu = null; + } + } + [Fact(Skip = "The only reason this test was 'passing' before was that the author forgot to call Window.ApplyTemplate()")] public void Cancelling_Closing_Leaves_ContextMenuOpen() { From 5ad8c41f37a593f0d3861e5308fb45201f96fa4f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 5 Apr 2020 17:19:58 +0200 Subject: [PATCH 12/17] Fix NRE in ContextMenu. --- src/Avalonia.Controls/ContextMenu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index bb9b853c28..1735599988 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -75,7 +75,7 @@ namespace Avalonia.Controls { control.PointerReleased -= ControlPointerReleased; oldMenu._attachedControl = null; - ((ISetLogicalParent)oldMenu._popup).SetParent(null); + ((ISetLogicalParent)oldMenu._popup)?.SetParent(null); } if (e.NewValue is ContextMenu newMenu) From ab6720095469e8cc18310b307ffc01ea18b28f7f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Apr 2020 11:55:38 +0200 Subject: [PATCH 13/17] Implement finding common base type. Had to also change the unit test to use controls that all have the `Background` property in a common base type. --- .../AvaloniaXamlIlSelectorTransformer.cs | 31 +++++++++++++++++-- .../Xaml/StyleTests.cs | 14 ++++----- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index 96d78b5092..d5114244cf 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -311,8 +311,35 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers _selectors.Add(node); } - //TODO: actually find the type - public override IXamlIlType TargetType => _selectors.FirstOrDefault()?.TargetType; + public override IXamlIlType TargetType + { + get + { + IXamlIlType result = null; + + foreach (var selector in _selectors) + { + if (selector.TargetType == null) + { + return null; + } + else if (result == null) + { + result = selector.TargetType; + } + else + { + while (!result.IsAssignableFrom(selector.TargetType)) + { + result = result.BaseType; + } + } + } + + return result; + } + } + protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) { if (_selectors.Count == 0) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 95c5ac89e4..02f0d7072c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -316,24 +316,24 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml - - - +