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 diff --git a/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs b/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs index 38c8e123cd..988cc123f9 100644 --- a/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs @@ -27,7 +27,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) { 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) 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..26be85beb3 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; + private 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(ScrollViewer.DefaultSmallChange, 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..8bfcc1f9db 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -59,6 +59,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 +165,15 @@ namespace Avalonia.Controls nameof(VerticalScrollBarVisibility), ScrollBarVisibility.Auto); + internal const double DefaultSmallChange = 16; + + private IDisposable _childSubscription; + private ILogicalScrollable _logicalScrollable; private Size _extent; private Vector _offset; private Size _viewport; + private Size _largeChange; + private Size _smallChange = new Size(DefaultSmallChange, 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, new Size(DefaultSmallChange, 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 @@ public event EventHandler Invalidated; + static Brush() + { + AffectsRender(OpacityProperty); + } + /// /// Gets or sets the opacity of the brush. /// 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..d5114244cf 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); } } @@ -308,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.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs new file mode 100644 index 0000000000..757160a0a4 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs @@ -0,0 +1,178 @@ +// 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); + } + + [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); + } + + [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); + } + + [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); + } + + [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 + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register("Foo"); + + public string Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + } + } +} 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() { diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs index 31eaa6b18c..53591eda5b 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs @@ -101,7 +101,7 @@ namespace Avalonia.Controls.UnitTests target.UpdateChild(); - Assert.NotNull(scrollable.InvalidateScroll); + Assert.True(scrollable.HasScrollInvalidatedSubscriber); } [Fact] @@ -117,7 +117,7 @@ namespace Avalonia.Controls.UnitTests target.Content = null; target.UpdateChild(); - Assert.Null(scrollable.InvalidateScroll); + Assert.False(scrollable.HasScrollInvalidatedSubscriber); } [Fact] @@ -217,7 +217,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); scrollable.IsLogicalScrollEnabled = false; - scrollable.InvalidateScroll(); + scrollable.RaiseScrollInvalidated(EventArgs.Empty); target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); @@ -227,7 +227,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 0, 150, 150), scrollable.Bounds); scrollable.IsLogicalScrollEnabled = true; - scrollable.InvalidateScroll(); + scrollable.RaiseScrollInvalidated(EventArgs.Empty); target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); @@ -318,12 +318,20 @@ namespace Avalonia.Controls.UnitTests private Size _extent; private Vector _offset; private Size _viewport; + private EventHandler _scrollInvalidated; public bool CanHorizontallyScroll { get; set; } public bool CanVerticallyScroll { get; set; } public bool IsLogicalScrollEnabled { get; set; } = true; public Size AvailableSize { get; private set; } - public Action InvalidateScroll { get; set; } + + public bool HasScrollInvalidatedSubscriber => _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 diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 95525a27c6..02f0d7072c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -275,5 +275,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 = @" + + + + + +