From eb6bfd3de86dc8e5c909c5eb51c86e998f7c76c4 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 6 Jun 2017 00:46:47 +0300 Subject: [PATCH 001/211] Moved layout manager from service locator to ILayoutRoot --- src/Avalonia.Controls/Application.cs | 1 - .../Embedding/EmbeddableControlRoot.cs | 2 +- .../Presenters/ItemVirtualizerSimple.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 15 +++++++++- src/Avalonia.Controls/Window.cs | 4 +-- src/Avalonia.Controls/WindowBase.cs | 4 +-- src/Avalonia.Layout/ILayoutRoot.cs | 5 ++++ src/Avalonia.Layout/LayoutManager.cs | 5 ---- src/Avalonia.Layout/Layoutable.cs | 13 +++++++-- .../ItemsPresenterTests_Virtualization.cs | 12 ++++++-- ...emsPresenterTests_Virtualization_Simple.cs | 10 +++---- .../Primitives/PopupTests.cs | 1 - .../TopLevelTests.cs | 17 +++++++---- .../WindowBaseTests.cs | 2 +- .../FullLayoutTests.cs | 7 ++--- .../LayoutManagerTests.cs | 11 +++----- .../TestLayoutRoot.cs | 19 ++++++++++++- tests/Avalonia.LeakTests/ControlTests.cs | 28 +++++++++---------- tests/Avalonia.UnitTests/TestRoot.cs | 2 +- tests/Avalonia.UnitTests/TestServices.cs | 11 +------- tests/Avalonia.UnitTests/TestTemplatedRoot.cs | 2 +- .../Avalonia.UnitTests/UnitTestApplication.cs | 1 - 22 files changed, 104 insertions(+), 70 deletions(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 3d13608226..8122af36ce 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -175,7 +175,6 @@ namespace Avalonia .Bind().ToConstant(InputManager) .Bind().ToTransient() .Bind().ToConstant(_styler) - .Bind().ToSingleton() .Bind().ToConstant(this) .Bind().ToConstant(AvaloniaScheduler.Instance); } diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index b8d54fa67b..d06172b4b3 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -26,7 +26,7 @@ namespace Avalonia.Controls.Embedding { EnsureInitialized(); ApplyTemplate(); - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(this); } private void EnsureInitialized() diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 20602d5475..38e4d2c5cc 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -510,7 +510,7 @@ namespace Avalonia.Controls.Presenters } var container = generator.ContainerFromIndex(index); - var layoutManager = LayoutManager.Instance; + var layoutManager = (Owner.GetVisualRoot() as ILayoutRoot)?.LayoutManager; // We need to do a layout here because it's possible that the container we moved to // is only partially visible due to differing item sizes. If the container is only diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index a0a8f6b27e..516b6f7e7d 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -46,6 +46,7 @@ namespace Avalonia.Controls private readonly IApplicationLifecycle _applicationLifecycle; private readonly IPlatformRenderInterface _renderInterface; private Size _clientSize; + private ILayoutManager _layoutManager; /// /// Initializes static members of the class. @@ -133,6 +134,18 @@ namespace Avalonia.Controls protected set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); } } + protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(); + + public ILayoutManager LayoutManager + { + get + { + if (_layoutManager == null) + _layoutManager = CreateLayoutManager(); + return _layoutManager; + } + } + /// /// Gets the platform-specific window implementation. /// @@ -245,7 +258,7 @@ namespace Avalonia.Controls ClientSize = clientSize; Width = clientSize.Width; Height = clientSize.Height; - LayoutManager.Instance.ExecuteLayoutPass(); + LayoutManager.ExecuteLayoutPass(); Renderer?.Resized(clientSize); } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 3802f2b6ea..3421bd3f32 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -247,7 +247,7 @@ namespace Avalonia.Controls EnsureInitialized(); IsVisible = true; - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(this); using (BeginAutoSizing()) { @@ -286,7 +286,7 @@ namespace Avalonia.Controls EnsureInitialized(); IsVisible = true; - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(this); using (BeginAutoSizing()) { diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 1f484fd6cb..990d4d201c 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -136,7 +136,7 @@ namespace Avalonia.Controls { EnsureInitialized(); IsVisible = true; - LayoutManager.Instance.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(this); PlatformImpl?.Show(); } finally @@ -215,7 +215,7 @@ namespace Avalonia.Controls Height = clientSize.Height; } ClientSize = clientSize; - LayoutManager.Instance.ExecuteLayoutPass(); + LayoutManager.ExecuteLayoutPass(); Renderer?.Resized(clientSize); } diff --git a/src/Avalonia.Layout/ILayoutRoot.cs b/src/Avalonia.Layout/ILayoutRoot.cs index 25a6331b38..700b6a8600 100644 --- a/src/Avalonia.Layout/ILayoutRoot.cs +++ b/src/Avalonia.Layout/ILayoutRoot.cs @@ -22,5 +22,10 @@ namespace Avalonia.Layout /// The scaling factor to use in layout. /// double LayoutScaling { get; } + + /// + /// Associated instance of layout manager + /// + ILayoutManager LayoutManager { get; } } } diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index b7b83bf852..ebf6411beb 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -19,11 +19,6 @@ namespace Avalonia.Layout private bool _queued; private bool _running; - /// - /// Gets the layout manager. - /// - public static ILayoutManager Instance => AvaloniaLocator.Current.GetService(); - /// public void InvalidateMeasure(ILayoutable control) { diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 20050058bf..bea62efe50 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -378,7 +378,7 @@ namespace Avalonia.Layout IsMeasureValid = false; IsArrangeValid = false; - LayoutManager.Instance?.InvalidateMeasure(this); + (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateMeasure(this); InvalidateVisual(); } } @@ -393,7 +393,7 @@ namespace Avalonia.Layout Logger.Verbose(LogArea.Layout, this, "Invalidated arrange"); IsArrangeValid = false; - LayoutManager.Instance?.InvalidateArrange(this); + (VisualRoot as ILayoutRoot)?.LayoutManager?.InvalidateArrange(this); InvalidateVisual(); } } @@ -620,6 +620,15 @@ namespace Avalonia.Layout base.OnVisualParentChanged(oldParent, newParent); } + protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTreeCore(e); + if(!IsMeasureValid) + (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateMeasure(this); + else if (!IsArrangeValid) + (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateArrange(this); + } + /// /// Calls on the control on which a property changed. /// diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 1ea64b915c..cb0fc705ce 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -12,6 +12,7 @@ using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests.Presenters @@ -219,7 +220,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Changing_VirtualizationMode_None_To_Simple_Should_Add_Correct_Number_Of_Controls() { - using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + using (UnitTestApplication.Start(new TestServices())) { var target = CreateTarget(mode: ItemVirtualizationMode.None); var scroll = (ScrollContentPresenter)target.Parent; @@ -237,7 +238,7 @@ namespace Avalonia.Controls.UnitTests.Presenters }; target.VirtualizationMode = ItemVirtualizationMode.Simple; - LayoutManager.Instance.ExecuteLayoutPass(); + ((ILayoutRoot)scroll.GetVisualRoot()).LayoutManager.ExecuteLayoutPass(); Assert.Equal(10, target.Panel.Children.Count); } @@ -313,11 +314,16 @@ namespace Avalonia.Controls.UnitTests.Presenters }); } - private class TestScroller : ScrollContentPresenter, IRenderRoot + private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot { public IRenderer Renderer { get; } public Size ClientSize { get; } + public Size MaxClientSize => Size.Infinity; + + public double LayoutScaling => 1; + + public ILayoutManager LayoutManager { get; } = new LayoutManager(); public IRenderTarget CreateRenderTarget() { throw new NotImplementedException(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 1da9cfce76..b18729160b 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -564,11 +564,10 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Scrolling_To_Item_In_Zero_Sized_Presenter_Doesnt_Throw() { - using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + using (UnitTestApplication.Start(new TestServices())) { var target = CreateTarget(itemCount: 10); var items = (IList)target.Items; - target.ApplyTemplate(); target.Measure(Size.Empty); target.Arrange(Rect.Empty); @@ -757,7 +756,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Down_Should_Scroll_If_Partially_Visible() { - using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + using (UnitTestApplication.Start(new TestServices())) { var target = CreateTarget(); var scroller = (ScrollContentPresenter)target.Parent; @@ -778,7 +777,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() { - using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + using (UnitTestApplication.Start(new TestServices())) { var target = CreateTarget(); var scroller = (ScrollContentPresenter)target.Parent; @@ -869,7 +868,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Right_Should_Scroll_If_Partially_Visible() { - using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + using (UnitTestApplication.Start(new TestServices())) { var target = CreateTarget(orientation: Orientation.Horizontal); var scroller = (ScrollContentPresenter)target.Parent; @@ -1008,6 +1007,7 @@ namespace Avalonia.Controls.UnitTests.Presenters }; scroller.UpdateChild(); + new TestRoot().Child = scroller; return result; } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index f192e87f08..6696258258 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -270,7 +270,6 @@ namespace Avalonia.Controls.UnitTests.Primitives var renderInterface = new Mock(); AvaloniaLocator.CurrentMutable - .Bind().ToTransient() .Bind().ToFunc(() => globalStyles.Object) .Bind().ToConstant(new WindowingPlatformMock()) .Bind().ToTransient() diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 5cd3c57e2e..35b0c75a08 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -65,15 +65,16 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Layout_Pass_Should_Not_Be_Automatically_Scheduled() { - var services = TestServices.StyledWindow.With(layoutManager: Mock.Of()); + var services = TestServices.StyledWindow; using (UnitTestApplication.Start(services)) { var impl = new Mock(); - var target = new TestTopLevel(impl.Object); + + var target = new TestTopLevel(impl.Object, Mock.Of()); // The layout pass should be scheduled by the derived class. - var layoutManagerMock = Mock.Get(LayoutManager.Instance); + var layoutManagerMock = Mock.Get(target.LayoutManager); layoutManagerMock.Verify(x => x.ExecuteLayoutPass(), Times.Never); } } @@ -98,7 +99,7 @@ namespace Avalonia.Controls.UnitTests } }; - LayoutManager.Instance.ExecuteInitialLayoutPass(target); + target.LayoutManager.ExecuteInitialLayoutPass(target); Assert.Equal(new Rect(0, 0, 321, 432), target.Bounds); } @@ -113,7 +114,7 @@ namespace Avalonia.Controls.UnitTests impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); var target = new TestTopLevel(impl.Object); - LayoutManager.Instance.ExecuteLayoutPass(); + target.LayoutManager.ExecuteLayoutPass(); Assert.Equal(double.NaN, target.Width); Assert.Equal(double.NaN, target.Height); @@ -222,13 +223,17 @@ namespace Avalonia.Controls.UnitTests private class TestTopLevel : TopLevel { + private readonly ILayoutManager _layoutManager; public bool IsClosed { get; private set; } - public TestTopLevel(ITopLevelImpl impl) + public TestTopLevel(ITopLevelImpl impl, ILayoutManager layoutManager = null) : base(impl) { + _layoutManager = layoutManager ?? new LayoutManager(); } + protected override ILayoutManager CreateLayoutManager() => _layoutManager; + protected override void HandleApplicationExiting() { base.HandleApplicationExiting(); diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index d6ffb32d1c..3c3070db22 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -38,7 +38,7 @@ namespace Avalonia.Controls.UnitTests IsVisible = true, }; - LayoutManager.Instance.ExecuteInitialLayoutPass(target); + target.LayoutManager.ExecuteInitialLayoutPass(target); Mock.Get(impl).Verify(x => x.Resize(new Size(321, 432))); } diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index 6b7c73da2a..7d79d369cb 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -56,11 +56,11 @@ namespace Avalonia.Layout.UnitTests }; window.Show(); - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.Equal(new Size(400, 400), border.Bounds.Size); textBlock.Width = 200; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Equal(new Size(200, 400), border.Bounds.Size); } @@ -98,7 +98,7 @@ namespace Avalonia.Layout.UnitTests }; window.Show(); - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.Equal(new Size(800, 600), window.Bounds.Size); Assert.Equal(new Size(200, 200), scrollViewer.Bounds.Size); @@ -186,7 +186,6 @@ namespace Avalonia.Layout.UnitTests .Bind().ToConstant(new AssetLoader()) .Bind().ToConstant(new Mock().Object) .Bind().ToConstant(globalStyles.Object) - .Bind().ToConstant(new LayoutManager()) .Bind().ToConstant(new AppBuilder().RuntimePlatform) .Bind().ToConstant(renderInterface.Object) .Bind().ToConstant(new Styler()) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index f67c5a353f..d76c9ea6bc 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -11,12 +11,9 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Invalidating_Child_Should_Remeasure_Parent() { - var layoutManager = new LayoutManager(); - using (AvaloniaLocator.EnterScope()) { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); - + Border border; StackPanel panel; @@ -30,14 +27,14 @@ namespace Avalonia.Layout.UnitTests } } }; - - layoutManager.ExecuteInitialLayoutPass(root); + + root.LayoutManager.ExecuteInitialLayoutPass(root); Assert.Equal(new Size(0, 0), root.DesiredSize); border.Width = 100; border.Height = 100; - layoutManager.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.Equal(new Size(100, 100), panel.DesiredSize); } } diff --git a/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs b/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs index fab1647c5d..8c4dfbb209 100644 --- a/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs +++ b/tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs @@ -2,10 +2,12 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Controls; +using Avalonia.Platform; +using Avalonia.Rendering; namespace Avalonia.Layout.UnitTests { - internal class TestLayoutRoot : Decorator, ILayoutRoot + internal class TestLayoutRoot : Decorator, ILayoutRoot, IRenderRoot { public TestLayoutRoot() { @@ -18,7 +20,22 @@ namespace Avalonia.Layout.UnitTests set; } + public IRenderer Renderer => null; + + public IRenderTarget CreateRenderTarget() => null; + + public void Invalidate(Rect rect) + { + } + + public Point PointToClient(Point point) => point; + + public Point PointToScreen(Point point) => point; + public Size MaxClientSize => Size.Infinity; public double LayoutScaling => 1; + + public ILayoutManager LayoutManager { get; set; } = new LayoutManager(); + } } diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index b9bdee09e6..fa004976c0 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -42,12 +42,12 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that Canvas gets added to visual tree. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.IsType(window.Presenter.Child); // Clear the content and ensure the Canvas is removed. window.Content = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); return window; @@ -78,13 +78,13 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that Canvas gets added to visual tree. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.IsType(window.Find("foo")); Assert.IsType(window.Presenter.Child); // Clear the content and ensure the Canvas is removed. window.Content = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); return window; @@ -116,13 +116,13 @@ namespace Avalonia.LeakTests // Do a layout and make sure that ScrollViewer gets added to visual tree and its // template applied. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.IsType(window.Presenter.Child); Assert.IsType(((ScrollViewer)window.Presenter.Child).Presenter.Child); // Clear the content and ensure the ScrollViewer is removed. window.Content = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); return window; @@ -153,13 +153,13 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // template applied. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.IsType(window.Presenter.Child); Assert.NotEqual(0, window.Presenter.Child.GetVisualChildren().Count()); // Clear the content and ensure the TextBox is removed. window.Content = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); return window; @@ -197,14 +197,14 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // Text property set. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.IsType(window.Presenter.Child); Assert.Equal("foo", ((TextBox)window.Presenter.Child).Text); // Clear the content and DataContext and ensure the TextBox is removed. window.Content = null; window.DataContext = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); return window; @@ -235,7 +235,7 @@ namespace Avalonia.LeakTests // Do a layout and make sure that TextBox gets added to visual tree and its // template applied. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.Same(textBox, window.Presenter.Child); // Get the border from the TextBox template. @@ -247,7 +247,7 @@ namespace Avalonia.LeakTests // Clear the content and ensure the TextBox is removed. window.Content = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); // Check that the TextBox has no subscriptions to its Classes collection. @@ -289,12 +289,12 @@ namespace Avalonia.LeakTests window.Show(); // Do a layout and make sure that TreeViewItems get realized. - LayoutManager.Instance.ExecuteInitialLayoutPass(window); + window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.Equal(1, target.ItemContainerGenerator.Containers.Count()); // Clear the content and ensure the TreeView is removed. window.Content = null; - LayoutManager.Instance.ExecuteLayoutPass(); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); return window; diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 8a711c415e..7387602213 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -47,7 +47,7 @@ namespace Avalonia.UnitTests public double LayoutScaling => 1; - public ILayoutManager LayoutManager => AvaloniaLocator.Current.GetService(); + public ILayoutManager LayoutManager { get; set; } = new LayoutManager(); public IRenderTarget RenderTarget => null; diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 0cd8d4295b..6699c33be9 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -21,7 +21,6 @@ namespace Avalonia.UnitTests { public static readonly TestServices StyledWindow = new TestServices( assetLoader: new AssetLoader(), - layoutManager: new LayoutManager(), platform: new AppBuilder().RuntimePlatform, renderer: (_, __) => Mock.Of(), renderInterface: CreateRenderInterfaceMock(), @@ -51,10 +50,7 @@ namespace Avalonia.UnitTests focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(), inputManager: new InputManager()); - - public static readonly TestServices RealLayoutManager = new TestServices( - layoutManager: new LayoutManager()); - + public static readonly TestServices RealStyler = new TestServices( styler: new Styler()); @@ -63,7 +59,6 @@ namespace Avalonia.UnitTests IFocusManager focusManager = null, IInputManager inputManager = null, Func keyboardDevice = null, - ILayoutManager layoutManager = null, IRuntimePlatform platform = null, Func renderer = null, IPlatformRenderInterface renderInterface = null, @@ -79,7 +74,6 @@ namespace Avalonia.UnitTests FocusManager = focusManager; InputManager = inputManager; KeyboardDevice = keyboardDevice; - LayoutManager = layoutManager; Platform = platform; Renderer = renderer; RenderInterface = renderInterface; @@ -96,7 +90,6 @@ namespace Avalonia.UnitTests public IInputManager InputManager { get; } public IFocusManager FocusManager { get; } public Func KeyboardDevice { get; } - public ILayoutManager LayoutManager { get; } public IRuntimePlatform Platform { get; } public Func Renderer { get; } public IPlatformRenderInterface RenderInterface { get; } @@ -113,7 +106,6 @@ namespace Avalonia.UnitTests IFocusManager focusManager = null, IInputManager inputManager = null, Func keyboardDevice = null, - ILayoutManager layoutManager = null, IRuntimePlatform platform = null, Func renderer = null, IPlatformRenderInterface renderInterface = null, @@ -131,7 +123,6 @@ namespace Avalonia.UnitTests focusManager: focusManager ?? FocusManager, inputManager: inputManager ?? InputManager, keyboardDevice: keyboardDevice ?? KeyboardDevice, - layoutManager: layoutManager ?? LayoutManager, platform: platform ?? Platform, renderer: renderer ?? Renderer, renderInterface: renderInterface ?? RenderInterface, diff --git a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs index 73c9f370d6..8bed09dd27 100644 --- a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs +++ b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs @@ -39,7 +39,7 @@ namespace Avalonia.UnitTests public double LayoutScaling => 1; - public ILayoutManager LayoutManager => AvaloniaLocator.Current.GetService(); + public ILayoutManager LayoutManager { get; set; } = new LayoutManager(); public IRenderTarget RenderTarget => null; diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index c5d533486b..22d0901a14 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -49,7 +49,6 @@ namespace Avalonia.UnitTests .BindToSelf(this) .Bind().ToConstant(Services.InputManager) .Bind().ToConstant(Services.KeyboardDevice?.Invoke()) - .Bind().ToConstant(Services.LayoutManager) .Bind().ToConstant(Services.Platform) .Bind().ToConstant(new RendererFactory(Services.Renderer)) .Bind().ToConstant(Services.RenderInterface) From 00fbc5cea70b6925b1fa121f3868cc1d300f6b2d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 6 Jun 2017 01:30:21 +0300 Subject: [PATCH 002/211] Use (0, 0) as default for non-existing PreviousMeasure --- src/Avalonia.Layout/LayoutManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index ebf6411beb..ed2b930114 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -133,7 +133,7 @@ namespace Avalonia.Layout if (!control.IsMeasureValid) { - control.Measure(control.PreviousMeasure.Value); + control.Measure(control.PreviousMeasure ?? default(Size)); } _toMeasure.Remove(control); From 48df92055ec66285e04683b22034a748d549fe43 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 11 Jun 2017 13:36:06 +0200 Subject: [PATCH 003/211] Fix ItemsPresenterSimple tests. There were two problems: - There were two root controls (`TestScroller` and `TestRoot`) - The root control needs to have a fixed size otherwise it will grow because `LayoutManager` passes `MaxClientSize` to its measure (which may be different to the initial measure that we were using to set its size). --- ...emsPresenterTests_Virtualization_Simple.cs | 121 +++++++++--------- 1 file changed, 63 insertions(+), 58 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index b18729160b..7a7f4ab4ec 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -12,6 +12,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.UnitTests; @@ -722,10 +723,10 @@ namespace Avalonia.Controls.UnitTests.Presenters public void GetControlInDirection_Down_Should_Return_Existing_Container_If_Materialized() { var target = CreateTarget(); + var scroller = (TestScroller)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + scroller.Width = scroller.Height = 100; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); var from = target.Panel.Children[5]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -739,10 +740,10 @@ namespace Avalonia.Controls.UnitTests.Presenters public void GetControlInDirection_Down_Should_Scroll_If_Necessary() { var target = CreateTarget(); + var scroller = (TestScroller)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + scroller.Width = scroller.Height = 100; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); var from = target.Panel.Children[9]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -756,44 +757,40 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Down_Should_Scroll_If_Partially_Visible() { - using (UnitTestApplication.Start(new TestServices())) - { - var target = CreateTarget(); - var scroller = (ScrollContentPresenter)target.Parent; + var target = CreateTarget(); + var scroller = (TestScroller)target.Parent; - scroller.Measure(new Size(100, 95)); - scroller.Arrange(new Rect(0, 0, 100, 95)); + scroller.Width = 100; + scroller.Height = 95; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); - var from = target.Panel.Children[8]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Down, - from); + var from = target.Panel.Children[8]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + NavigationDirection.Down, + from); - Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[8], result); - } + Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[8], result); } [Fact] public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() { - using (UnitTestApplication.Start(new TestServices())) - { - var target = CreateTarget(); - var scroller = (ScrollContentPresenter)target.Parent; + var target = CreateTarget(); + var scroller = (TestScroller)target.Parent; - scroller.Measure(new Size(100, 95)); - scroller.Arrange(new Rect(0, 0, 100, 95)); - ((ILogicalScrollable)target).Offset = new Vector(0, 11); + scroller.Width = 100; + scroller.Height = 95; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); + ((ILogicalScrollable)target).Offset = new Vector(0, 11); - var from = target.Panel.Children[1]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Up, - from); + var from = target.Panel.Children[1]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + NavigationDirection.Up, + from); - Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[0], result); - } + Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[0], result); } [Fact] @@ -834,10 +831,10 @@ namespace Avalonia.Controls.UnitTests.Presenters public void GetControlInDirection_Right_Should_Return_Existing_Container_If_Materialized() { var target = CreateTarget(orientation: Orientation.Horizontal); + var scroller = (TestScroller)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + scroller.Width = scroller.Height = 100; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); var from = target.Panel.Children[5]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -851,10 +848,10 @@ namespace Avalonia.Controls.UnitTests.Presenters public void GetControlInDirection_Right_Should_Scroll_If_Necessary() { var target = CreateTarget(orientation: Orientation.Horizontal); + var scroller = (TestScroller)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + scroller.Width = scroller.Height = 100; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); var from = target.Panel.Children[9]; var result = ((ILogicalScrollable)target).GetControlInDirection( @@ -868,32 +865,31 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Right_Should_Scroll_If_Partially_Visible() { - using (UnitTestApplication.Start(new TestServices())) - { - var target = CreateTarget(orientation: Orientation.Horizontal); - var scroller = (ScrollContentPresenter)target.Parent; + var target = CreateTarget(orientation: Orientation.Horizontal); + var scroller = (TestScroller)target.Parent; - scroller.Measure(new Size(95, 100)); - scroller.Arrange(new Rect(0, 0, 95, 100)); + scroller.Width = 95; + scroller.Height = 100; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); - var from = target.Panel.Children[8]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Right, - from); + var from = target.Panel.Children[8]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + NavigationDirection.Right, + from); - Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[8], result); - } + Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[8], result); } [Fact] public void GetControlInDirection_Left_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() { var target = CreateTarget(orientation: Orientation.Horizontal); + var scroller = (TestScroller)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(95, 100)); - target.Arrange(new Rect(0, 0, 95, 100)); + scroller.Width = 95; + scroller.Height = 100; + scroller.LayoutManager.ExecuteInitialLayoutPass(scroller); ((ILogicalScrollable)target).Offset = new Vector(11, 0); var from = target.Panel.Children[1]; @@ -1007,8 +1003,6 @@ namespace Avalonia.Controls.UnitTests.Presenters }; scroller.UpdateChild(); - new TestRoot().Child = scroller; - return result; } @@ -1030,11 +1024,17 @@ namespace Avalonia.Controls.UnitTests.Presenters }); } - private class TestScroller : ScrollContentPresenter, IRenderRoot + private class TestScroller : ScrollContentPresenter, IRenderRoot, ILayoutRoot { public IRenderer Renderer { get; } public Size ClientSize { get; } + public Size MaxClientSize => Size.Infinity; + + public double LayoutScaling => 1; + + public ILayoutManager LayoutManager { get; } = new LayoutManager(); + public IRenderTarget CreateRenderTarget() { throw new NotImplementedException(); @@ -1054,6 +1054,11 @@ namespace Avalonia.Controls.UnitTests.Presenters { throw new NotImplementedException(); } + + protected override Size MeasureOverride(Size availableSize) + { + return base.MeasureOverride(availableSize); + } } private class TestItemsPresenter : ItemsPresenter From 4b8db11f0d237fd4e1b8bfbeaac1ab399514631f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 11 Jun 2017 13:38:16 +0200 Subject: [PATCH 004/211] FIx ItemsPresenterTests_Virtualization. The root control must have a fixed size, doing an initial measure isn't enough as the `LayoutManager` will remeasure with `MaxClientSize`. --- .../Presenters/ItemsPresenterTests_Virtualization.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index cb0fc705ce..c69eebf324 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -223,10 +223,10 @@ namespace Avalonia.Controls.UnitTests.Presenters using (UnitTestApplication.Start(new TestServices())) { var target = CreateTarget(mode: ItemVirtualizationMode.None); - var scroll = (ScrollContentPresenter)target.Parent; + var scroll = (TestScroller)target.Parent; - scroll.Measure(new Size(100, 100)); - scroll.Arrange(new Rect(0, 0, 100, 100)); + scroll.Width = scroll.Height = 100; + scroll.LayoutManager.ExecuteInitialLayoutPass(scroll); // Ensure than an intermediate measure pass doesn't add more controls than it // should. This can happen if target gets measured with Size.Infinity which From 188a0ce442d7b8fb073ebfb9c1f07b50d561c3c7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2017 01:45:49 +0200 Subject: [PATCH 005/211] Fix compile errors. --- tests/Avalonia.Benchmarks/Layout/Measure.cs | 14 +++----------- .../LayoutManagerTests.cs | 2 -- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/tests/Avalonia.Benchmarks/Layout/Measure.cs b/tests/Avalonia.Benchmarks/Layout/Measure.cs index d1fdae9971..b0490d8a0f 100644 --- a/tests/Avalonia.Benchmarks/Layout/Measure.cs +++ b/tests/Avalonia.Benchmarks/Layout/Measure.cs @@ -8,26 +8,18 @@ using BenchmarkDotNet.Attributes; namespace Avalonia.Benchmarks.Layout { [MemoryDiagnoser] - public class Measure : IDisposable + public class Measure { - private IDisposable _app; private TestRoot root; private List controls = new List(); public Measure() { - _app = UnitTestApplication.Start(TestServices.RealLayoutManager); - var panel = new StackPanel(); root = new TestRoot { Child = panel }; controls.Add(panel); CreateChildren(panel, 3, 5); - LayoutManager.Instance.ExecuteInitialLayoutPass(root); - } - - public void Dispose() - { - _app.Dispose(); + root.LayoutManager.ExecuteInitialLayoutPass(root); } [Benchmark] @@ -43,7 +35,7 @@ namespace Avalonia.Benchmarks.Layout } } - LayoutManager.Instance.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); } private void CreateChildren(IPanel parent, int childCount, int iterations) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index c4c0f9a441..5f43a8d00e 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -264,8 +264,6 @@ namespace Avalonia.Layout.UnitTests { using (AvaloniaLocator.EnterScope()) { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); - Border border; StackPanel panel; From 5e3f604308e514e3392160e307602288d9f1e53a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2017 01:51:11 +0200 Subject: [PATCH 006/211] Removed unnecessary service locator code. `ILayoutManager` is not longer retrieved from `AvaloniaLocator` so can remove this stuff. --- .../LayoutManagerTests.cs | 314 ++++++++---------- .../LayoutableTests.cs | 87 +++-- 2 files changed, 174 insertions(+), 227 deletions(-) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index 5f43a8d00e..3526b29cb5 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -15,285 +15,239 @@ namespace Avalonia.Layout.UnitTests public void Measures_And_Arranges_InvalidateMeasured_Control() { var target = new LayoutManager(); + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; - using (Start(target)) - { - var control = new LayoutTestControl(); - var root = new LayoutTestRoot { Child = control }; - - target.ExecuteInitialLayoutPass(root); - control.Measured = control.Arranged = false; + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; - control.InvalidateMeasure(); - target.ExecuteLayoutPass(); + control.InvalidateMeasure(); + target.ExecuteLayoutPass(); - Assert.True(control.Measured); - Assert.True(control.Arranged); - } + Assert.True(control.Measured); + Assert.True(control.Arranged); } [Fact] public void Arranges_InvalidateArranged_Control() { var target = new LayoutManager(); + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; - using (Start(target)) - { - var control = new LayoutTestControl(); - var root = new LayoutTestRoot { Child = control }; - - target.ExecuteInitialLayoutPass(root); - control.Measured = control.Arranged = false; + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; - control.InvalidateArrange(); - target.ExecuteLayoutPass(); + control.InvalidateArrange(); + target.ExecuteLayoutPass(); - Assert.False(control.Measured); - Assert.True(control.Arranged); - } + Assert.False(control.Measured); + Assert.True(control.Arranged); } [Fact] public void Measures_Parent_Of_Newly_Added_Control() { var target = new LayoutManager(); + var control = new LayoutTestControl(); + var root = new LayoutTestRoot(); - using (Start(target)) - { - var control = new LayoutTestControl(); - var root = new LayoutTestRoot(); - - target.ExecuteInitialLayoutPass(root); - root.Child = control; - root.Measured = root.Arranged = false; + target.ExecuteInitialLayoutPass(root); + root.Child = control; + root.Measured = root.Arranged = false; - target.ExecuteLayoutPass(); + target.ExecuteLayoutPass(); - Assert.True(root.Measured); - Assert.True(root.Arranged); - Assert.True(control.Measured); - Assert.True(control.Arranged); - } + Assert.True(root.Measured); + Assert.True(root.Arranged); + Assert.True(control.Measured); + Assert.True(control.Arranged); } [Fact] public void Measures_In_Correct_Order() { var target = new LayoutManager(); - - using (Start(target)) + LayoutTestControl control1; + LayoutTestControl control2; + var root = new LayoutTestRoot { - LayoutTestControl control1; - LayoutTestControl control2; - var root = new LayoutTestRoot + Child = control1 = new LayoutTestControl { - Child = control1 = new LayoutTestControl - { - Child = control2 = new LayoutTestControl(), - } - }; + Child = control2 = new LayoutTestControl(), + } + }; - var order = new List(); - Size MeasureOverride(ILayoutable control, Size size) - { - order.Add(control); - return new Size(10, 10); - } + var order = new List(); + Size MeasureOverride(ILayoutable control, Size size) + { + order.Add(control); + return new Size(10, 10); + } - root.DoMeasureOverride = MeasureOverride; - control1.DoMeasureOverride = MeasureOverride; - control2.DoMeasureOverride = MeasureOverride; - target.ExecuteInitialLayoutPass(root); + root.DoMeasureOverride = MeasureOverride; + control1.DoMeasureOverride = MeasureOverride; + control2.DoMeasureOverride = MeasureOverride; + target.ExecuteInitialLayoutPass(root); - control2.InvalidateMeasure(); - control1.InvalidateMeasure(); - root.InvalidateMeasure(); + control2.InvalidateMeasure(); + control1.InvalidateMeasure(); + root.InvalidateMeasure(); - order.Clear(); - target.ExecuteLayoutPass(); + order.Clear(); + target.ExecuteLayoutPass(); - Assert.Equal(new ILayoutable[] { root, control1, control2 }, order); - } + Assert.Equal(new ILayoutable[] { root, control1, control2 }, order); } [Fact] public void Measures_Root_And_Grandparent_In_Correct_Order() { var target = new LayoutManager(); - - using (Start(target)) + LayoutTestControl control1; + LayoutTestControl control2; + var root = new LayoutTestRoot { - LayoutTestControl control1; - LayoutTestControl control2; - var root = new LayoutTestRoot + Child = control1 = new LayoutTestControl { - Child = control1 = new LayoutTestControl - { - Child = control2 = new LayoutTestControl(), - } - }; + Child = control2 = new LayoutTestControl(), + } + }; - var order = new List(); - Size MeasureOverride(ILayoutable control, Size size) - { - order.Add(control); - return new Size(10, 10); - } + var order = new List(); + Size MeasureOverride(ILayoutable control, Size size) + { + order.Add(control); + return new Size(10, 10); + } - root.DoMeasureOverride = MeasureOverride; - control1.DoMeasureOverride = MeasureOverride; - control2.DoMeasureOverride = MeasureOverride; - target.ExecuteInitialLayoutPass(root); + root.DoMeasureOverride = MeasureOverride; + control1.DoMeasureOverride = MeasureOverride; + control2.DoMeasureOverride = MeasureOverride; + target.ExecuteInitialLayoutPass(root); - control2.InvalidateMeasure(); - root.InvalidateMeasure(); + control2.InvalidateMeasure(); + root.InvalidateMeasure(); - order.Clear(); - target.ExecuteLayoutPass(); + order.Clear(); + target.ExecuteLayoutPass(); - Assert.Equal(new ILayoutable[] { root, control2 }, order); - } + Assert.Equal(new ILayoutable[] { root, control2 }, order); } [Fact] public void Doesnt_Measure_Non_Invalidated_Root() { var target = new LayoutManager(); + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; - using (Start(target)) - { - var control = new LayoutTestControl(); - var root = new LayoutTestRoot { Child = control }; + target.ExecuteInitialLayoutPass(root); + root.Measured = root.Arranged = false; + control.Measured = control.Arranged = false; - target.ExecuteInitialLayoutPass(root); - root.Measured = root.Arranged = false; - control.Measured = control.Arranged = false; + control.InvalidateMeasure(); + target.ExecuteLayoutPass(); - control.InvalidateMeasure(); - target.ExecuteLayoutPass(); - - Assert.False(root.Measured); - Assert.False(root.Arranged); - Assert.True(control.Measured); - Assert.True(control.Arranged); - } + Assert.False(root.Measured); + Assert.False(root.Arranged); + Assert.True(control.Measured); + Assert.True(control.Arranged); } [Fact] public void Doesnt_Measure_Removed_Control() { var target = new LayoutManager(); + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; - using (Start(target)) - { - var control = new LayoutTestControl(); - var root = new LayoutTestRoot { Child = control }; - - target.ExecuteInitialLayoutPass(root); - control.Measured = control.Arranged = false; + target.ExecuteInitialLayoutPass(root); + control.Measured = control.Arranged = false; - control.InvalidateMeasure(); - root.Child = null; - target.ExecuteLayoutPass(); + control.InvalidateMeasure(); + root.Child = null; + target.ExecuteLayoutPass(); - Assert.False(control.Measured); - Assert.False(control.Arranged); - } + Assert.False(control.Measured); + Assert.False(control.Arranged); } [Fact] public void Measures_Root_With_Infinity() { var target = new LayoutManager(); + var root = new LayoutTestRoot(); + var availableSize = default(Size); - using (Start(target)) - { - var root = new LayoutTestRoot(); - var availableSize = default(Size); - - // Should not measure with this size. - root.MaxClientSize = new Size(123, 456); + // Should not measure with this size. + root.MaxClientSize = new Size(123, 456); - root.DoMeasureOverride = (_, s) => - { - availableSize = s; - return new Size(100, 100); - }; + root.DoMeasureOverride = (_, s) => + { + availableSize = s; + return new Size(100, 100); + }; - target.ExecuteInitialLayoutPass(root); + target.ExecuteInitialLayoutPass(root); - Assert.Equal(Size.Infinity, availableSize); - } + Assert.Equal(Size.Infinity, availableSize); } [Fact] public void Arranges_Root_With_DesiredSize() { var target = new LayoutManager(); - - using (Start(target)) + var root = new LayoutTestRoot { - var root = new LayoutTestRoot - { - Width = 100, - Height = 100, - }; + Width = 100, + Height = 100, + }; - var arrangeSize = default(Size); + var arrangeSize = default(Size); - root.DoArrangeOverride = (_, s) => - { - arrangeSize = s; - return s; - }; + root.DoArrangeOverride = (_, s) => + { + arrangeSize = s; + return s; + }; - target.ExecuteInitialLayoutPass(root); - Assert.Equal(new Size(100, 100), arrangeSize); + target.ExecuteInitialLayoutPass(root); + Assert.Equal(new Size(100, 100), arrangeSize); - root.Width = 120; + root.Width = 120; - target.ExecuteLayoutPass(); - Assert.Equal(new Size(120, 100), arrangeSize); - } + target.ExecuteLayoutPass(); + Assert.Equal(new Size(120, 100), arrangeSize); } [Fact] public void Invalidating_Child_Remeasures_Parent() { - using (AvaloniaLocator.EnterScope()) - { - Border border; - StackPanel panel; + Border border; + StackPanel panel; - var root = new LayoutTestRoot + var root = new LayoutTestRoot + { + Child = panel = new StackPanel + { + Children = new Controls.Controls { - Child = panel = new StackPanel - { - Children = new Controls.Controls - { - (border = new Border()) - } - } - }; + (border = new Border()) + } + } + }; - root.LayoutManager.ExecuteInitialLayoutPass(root); - Assert.Equal(new Size(0, 0), root.DesiredSize); + root.LayoutManager.ExecuteInitialLayoutPass(root); + Assert.Equal(new Size(0, 0), root.DesiredSize); - border.Width = 100; - border.Height = 100; + border.Width = 100; + border.Height = 100; - root.LayoutManager.ExecuteLayoutPass(); - Assert.Equal(new Size(100, 100), panel.DesiredSize); - } - } - - private IDisposable Start(LayoutManager layoutManager) - { - var result = AvaloniaLocator.EnterScope(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); - return result; + root.LayoutManager.ExecuteLayoutPass(); + Assert.Equal(new Size(100, 100), panel.DesiredSize); } } } diff --git a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs index dcc65edc74..68cdaa1b12 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutableTests.cs @@ -11,80 +11,73 @@ namespace Avalonia.Layout.UnitTests public void Only_Calls_LayoutManager_InvalidateMeasure_Once() { var target = new Mock(); - - using (Start(target.Object)) + var control = new Decorator(); + var root = new LayoutTestRoot { - var control = new Decorator(); - var root = new LayoutTestRoot { Child = control }; + Child = control, + LayoutManager = target.Object, + }; - root.Measure(Size.Infinity); - root.Arrange(new Rect(root.DesiredSize)); - target.ResetCalls(); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + target.ResetCalls(); - control.InvalidateMeasure(); - control.InvalidateMeasure(); + control.InvalidateMeasure(); + control.InvalidateMeasure(); - target.Verify(x => x.InvalidateMeasure(control), Times.Once()); - } + target.Verify(x => x.InvalidateMeasure(control), Times.Once()); } [Fact] public void Only_Calls_LayoutManager_InvalidateArrange_Once() { var target = new Mock(); - - using (Start(target.Object)) + var control = new Decorator(); + var root = new LayoutTestRoot { - var control = new Decorator(); - var root = new LayoutTestRoot { Child = control }; + Child = control, + LayoutManager = target.Object, + }; - root.Measure(Size.Infinity); - root.Arrange(new Rect(root.DesiredSize)); - target.ResetCalls(); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + target.ResetCalls(); - control.InvalidateArrange(); - control.InvalidateArrange(); + control.InvalidateArrange(); + control.InvalidateArrange(); - target.Verify(x => x.InvalidateArrange(control), Times.Once()); - } + target.Verify(x => x.InvalidateArrange(control), Times.Once()); } [Fact] public void Attaching_Control_To_Tree_Invalidates_Parent_Measure() { var target = new Mock(); - - using (Start(target.Object)) + var control = new Decorator(); + var root = new LayoutTestRoot { - var control = new Decorator(); - var root = new LayoutTestRoot { Child = control }; + Child = control, + LayoutManager = target.Object, + }; - root.Measure(Size.Infinity); - root.Arrange(new Rect(root.DesiredSize)); - Assert.True(control.IsMeasureValid); + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + Assert.True(control.IsMeasureValid); - root.Child = null; - root.Measure(Size.Infinity); - root.Arrange(new Rect(root.DesiredSize)); + root.Child = null; + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); - Assert.False(control.IsMeasureValid); - Assert.True(root.IsMeasureValid); + Assert.False(control.IsMeasureValid); + Assert.True(root.IsMeasureValid); - target.ResetCalls(); + target.ResetCalls(); - root.Child = control; + root.Child = control; - Assert.False(root.IsMeasureValid); - Assert.False(control.IsMeasureValid); - target.Verify(x => x.InvalidateMeasure(root), Times.Once()); - } - } - - private IDisposable Start(ILayoutManager layoutManager) - { - var result = AvaloniaLocator.EnterScope(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(layoutManager); - return result; + Assert.False(root.IsMeasureValid); + Assert.False(control.IsMeasureValid); + target.Verify(x => x.InvalidateMeasure(root), Times.Once()); } } } From 061a264ca4fc0d2ff237fb86500a7577f42b020e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2017 01:55:46 +0200 Subject: [PATCH 007/211] Don't need to create LayoutManager target. There is already one on the root. --- .../LayoutManagerTests.cs | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index 3526b29cb5..70b5d5a991 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -14,15 +14,14 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Measures_And_Arranges_InvalidateMeasured_Control() { - var target = new LayoutManager(); var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); control.Measured = control.Arranged = false; control.InvalidateMeasure(); - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.True(control.Measured); Assert.True(control.Arranged); @@ -31,15 +30,14 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Arranges_InvalidateArranged_Control() { - var target = new LayoutManager(); var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); control.Measured = control.Arranged = false; control.InvalidateArrange(); - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.False(control.Measured); Assert.True(control.Arranged); @@ -48,15 +46,14 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Measures_Parent_Of_Newly_Added_Control() { - var target = new LayoutManager(); var control = new LayoutTestControl(); var root = new LayoutTestRoot(); - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); root.Child = control; root.Measured = root.Arranged = false; - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.True(root.Measured); Assert.True(root.Arranged); @@ -67,7 +64,6 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Measures_In_Correct_Order() { - var target = new LayoutManager(); LayoutTestControl control1; LayoutTestControl control2; var root = new LayoutTestRoot @@ -89,14 +85,14 @@ namespace Avalonia.Layout.UnitTests root.DoMeasureOverride = MeasureOverride; control1.DoMeasureOverride = MeasureOverride; control2.DoMeasureOverride = MeasureOverride; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); control2.InvalidateMeasure(); control1.InvalidateMeasure(); root.InvalidateMeasure(); order.Clear(); - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.Equal(new ILayoutable[] { root, control1, control2 }, order); } @@ -104,7 +100,6 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Measures_Root_And_Grandparent_In_Correct_Order() { - var target = new LayoutManager(); LayoutTestControl control1; LayoutTestControl control2; var root = new LayoutTestRoot @@ -126,13 +121,13 @@ namespace Avalonia.Layout.UnitTests root.DoMeasureOverride = MeasureOverride; control1.DoMeasureOverride = MeasureOverride; control2.DoMeasureOverride = MeasureOverride; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); control2.InvalidateMeasure(); root.InvalidateMeasure(); order.Clear(); - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.Equal(new ILayoutable[] { root, control2 }, order); } @@ -140,16 +135,15 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Doesnt_Measure_Non_Invalidated_Root() { - var target = new LayoutManager(); var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); root.Measured = root.Arranged = false; control.Measured = control.Arranged = false; control.InvalidateMeasure(); - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.False(root.Measured); Assert.False(root.Arranged); @@ -160,16 +154,15 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Doesnt_Measure_Removed_Control() { - var target = new LayoutManager(); var control = new LayoutTestControl(); var root = new LayoutTestRoot { Child = control }; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); control.Measured = control.Arranged = false; control.InvalidateMeasure(); root.Child = null; - target.ExecuteLayoutPass(); + root.LayoutManager.ExecuteLayoutPass(); Assert.False(control.Measured); Assert.False(control.Arranged); @@ -178,7 +171,6 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Measures_Root_With_Infinity() { - var target = new LayoutManager(); var root = new LayoutTestRoot(); var availableSize = default(Size); @@ -191,7 +183,7 @@ namespace Avalonia.Layout.UnitTests return new Size(100, 100); }; - target.ExecuteInitialLayoutPass(root); + root.LayoutManager.ExecuteInitialLayoutPass(root); Assert.Equal(Size.Infinity, availableSize); } @@ -199,7 +191,6 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Arranges_Root_With_DesiredSize() { - var target = new LayoutManager(); var root = new LayoutTestRoot { Width = 100, @@ -213,13 +204,13 @@ namespace Avalonia.Layout.UnitTests arrangeSize = s; return s; }; - - target.ExecuteInitialLayoutPass(root); + + root.LayoutManager.ExecuteInitialLayoutPass(root); Assert.Equal(new Size(100, 100), arrangeSize); root.Width = 120; - - target.ExecuteLayoutPass(); + + root.LayoutManager.ExecuteLayoutPass(); Assert.Equal(new Size(120, 100), arrangeSize); } From 5a34acac2d4588d8fce104019a99a76cb2c57cee Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2017 01:58:58 +0200 Subject: [PATCH 008/211] Don't need to do this. We don't need to invalidate newly added controls with the layout manager - this is already handed by the parent control. --- src/Avalonia.Layout/Layoutable.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index bea62efe50..f594450039 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -620,15 +620,6 @@ namespace Avalonia.Layout base.OnVisualParentChanged(oldParent, newParent); } - protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTreeCore(e); - if(!IsMeasureValid) - (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateMeasure(this); - else if (!IsArrangeValid) - (VisualRoot as ILayoutRoot)?.LayoutManager.InvalidateArrange(this); - } - /// /// Calls on the control on which a property changed. /// From ab30fd343b04382a34bc2e4e70b199f36df2792d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2017 02:00:12 +0200 Subject: [PATCH 009/211] Handle no previous measure/arrange. This shouldn't happen, but if it does, don't crash. --- src/Avalonia.Layout/LayoutManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index f1f0749d41..73aadcd545 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -165,7 +165,7 @@ namespace Avalonia.Layout { root.Measure(Size.Infinity); } - else + else if (control.PreviousMeasure.HasValue) { control.Measure(control.PreviousMeasure.Value); } @@ -185,7 +185,7 @@ namespace Avalonia.Layout { root.Arrange(new Rect(control.DesiredSize)); } - else + else if (control.PreviousArrange.HasValue) { control.Arrange(control.PreviousArrange.Value); } From e8c32bf801a6f36270155657b4665821019f986a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 17 May 2018 17:39:46 -0500 Subject: [PATCH 010/211] Preliminary support. Indexers require more work since the compiler doesn't generate IndexExpressions (they weren't in S.L.Expressions v1 so they aren't auto-genned). --- .../Data/ExpressionNodeBuilder.cs | 8 + .../Data/ExpressionObserver.cs | 92 ++++++++-- .../Data/ExpressionParseException.cs | 4 +- .../Data/IndexerExpressionNode.cs | 61 +++++++ .../Avalonia.Markup/Data/IndexerNode.cs | 80 ++------- .../Avalonia.Markup/Data/IndexerNodeBase.cs | 87 ++++++++++ .../Data/Parsers/ExpressionTreeParser.cs | 34 ++++ .../Parsers/ExpressionVisitorNodeBuilder.cs | 158 ++++++++++++++++++ .../ExpressionObserverTests_ExpressionTree.cs | 111 ++++++++++++ 9 files changed, 555 insertions(+), 80 deletions(-) create mode 100644 src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs create mode 100644 src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs create mode 100644 src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs create mode 100644 src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs create mode 100644 tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs b/src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs index 013299c1d7..e19259c6ed 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq.Expressions; using Avalonia.Markup.Data.Parsers; namespace Avalonia.Markup.Data @@ -26,5 +27,12 @@ namespace Avalonia.Markup.Data return node; } + + public static ExpressionNode Build(LambdaExpression expression, bool enableValidation = false) + { + var parser = new ExpressionTreeParser(enableValidation); + + return parser.Parse(expression); + } } } diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs index dd9718a0f6..127b338008 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -72,20 +73,36 @@ namespace Avalonia.Markup.Data string expression, bool enableDataValidation = false, string description = null) + : this(root, Parse(expression, enableDataValidation), description ?? expression) { Contract.Requires(expression != null); + Expression = expression; + } + private ExpressionObserver( + object root, + ExpressionNode node, + string description = null) + { if (root == AvaloniaProperty.UnsetValue) { root = null; } - Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); + _node = node; + Description = description; _root = new WeakReference(root); } + public static ExpressionObserver CreateFromExpression( + T root, + Expression> expression, + bool enableDataValidation = false, + string description = null) + { + return new ExpressionObserver(root, Parse(expression, enableDataValidation), description ?? expression.ToString()); + } + /// /// Initializes a new instance of the class. /// @@ -100,15 +117,38 @@ namespace Avalonia.Markup.Data string expression, bool enableDataValidation = false, string description = null) + : this(rootObservable, Parse(expression, enableDataValidation), description ?? expression) { Contract.Requires(rootObservable != null); Contract.Requires(expression != null); Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); - _finished = new Subject(); + } + + private ExpressionObserver( + IObservable rootObservable, + ExpressionNode node, + string description) + { + Contract.Requires(rootObservable != null); + + _node = node; + Description = description; _root = rootObservable; + _finished = new Subject(); + } + + public static ExpressionObserver CreateFromExpression( + IObservable rootObservable, + Expression> expression, + bool enableDataValidation = false, + string description = null) + { + Contract.Requires(rootObservable != null); + return new ExpressionObserver( + rootObservable.Select(o => (object)o), + Parse(expression, enableDataValidation), + description ?? expression.ToString()); } /// @@ -127,19 +167,44 @@ namespace Avalonia.Markup.Data IObservable update, bool enableDataValidation = false, string description = null) + : this(rootGetter, Parse(expression, enableDataValidation), update, description ?? expression) { - Contract.Requires(rootGetter != null); Contract.Requires(expression != null); - Contract.Requires(update != null); Expression = expression; - Description = description ?? expression; - _node = Parse(expression, enableDataValidation); - _finished = new Subject(); + } + + private ExpressionObserver( + Func rootGetter, + ExpressionNode node, + IObservable update, + string description) + { + Contract.Requires(rootGetter != null); + Contract.Requires(update != null); + Description = description; + _node = node; + _finished = new Subject(); _node.Target = new WeakReference(rootGetter()); _root = update.Select(x => rootGetter()); } + + public static ExpressionObserver CreateFromExpression( + Func rootGetter, + Expression> expression, + IObservable update, + bool enableDataValidation = false, + string description = null) + { + Contract.Requires(rootGetter != null); + + return new ExpressionObserver( + () => rootGetter(), + Parse(expression, enableDataValidation), + update, + description ?? expression.ToString()); + } /// /// Attempts to set the value of a property expression. @@ -238,6 +303,11 @@ namespace Avalonia.Markup.Data } } + private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation) + { + return ExpressionNodeBuilder.Build(expression, enableDataValidation); + } + private static object ToWeakReference(object o) { return o is BindingNotification ? o : new WeakReference(o); diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs b/src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs index d06bdd1e52..3ef225e70a 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionParseException.cs @@ -17,8 +17,8 @@ namespace Avalonia.Markup.Data /// /// The column position of the error. /// The exception message. - public ExpressionParseException(int column, string message) - : base(message) + public ExpressionParseException(int column, string message, Exception innerException = null) + : base(message, innerException) { Column = column; } diff --git a/src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs new file mode 100644 index 0000000000..b296badb86 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/IndexerExpressionNode.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Text; +using Avalonia.Data; + +namespace Avalonia.Markup.Data +{ + class IndexerExpressionNode : IndexerNodeBase + { + private readonly ParameterExpression parameter; + private readonly IndexExpression expression; + private readonly Delegate setDelegate; + private readonly Delegate getDelegate; + private readonly Delegate firstArgumentDelegate; + + public IndexerExpressionNode(IndexExpression expression) + { + parameter = Expression.Parameter(expression.Object.Type); + this.expression = expression.Update(parameter, expression.Arguments); + + getDelegate = Expression.Lambda(this.expression, parameter).Compile(); + + var valueParameter = Expression.Parameter(expression.Type); + + setDelegate = Expression.Lambda(Expression.Assign(this.expression, valueParameter), parameter, valueParameter).Compile(); + + firstArgumentDelegate = Expression.Lambda(this.expression.Arguments[0], parameter).Compile(); + } + + public override Type PropertyType => expression.Type; + + public override string Description => expression.ToString(); + + public override bool SetTargetValue(object value, BindingPriority priority) + { + try + { + setDelegate.DynamicInvoke(Target.Target, value); + return true; + } + catch (Exception) + { + return false; + } + } + + protected override object GetValue(object target) + { + return getDelegate.DynamicInvoke(target); + } + + protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e) + { + return expression.Indexer.Name == e.PropertyName; + } + + protected override int? TryGetFirstArgumentAsInt() => firstArgumentDelegate.DynamicInvoke(Target.Target) as int?; + } +} diff --git a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs index 4e2914a148..09ce2b85e9 100644 --- a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs @@ -15,7 +15,7 @@ using Avalonia.Data; namespace Avalonia.Markup.Data { - internal class IndexerNode : ExpressionNode, ISettableNode + internal class IndexerNode : IndexerNodeBase { public IndexerNode(IList arguments) { @@ -24,35 +24,7 @@ namespace Avalonia.Markup.Data public override string Description => "[" + string.Join(",", Arguments) + "]"; - protected override IObservable StartListeningCore(WeakReference reference) - { - var target = reference.Target; - var incc = target as INotifyCollectionChanged; - var inpc = target as INotifyPropertyChanged; - var inputs = new List>(); - - if (incc != null) - { - inputs.Add(WeakObservable.FromEventPattern( - incc, - nameof(incc.CollectionChanged)) - .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) - .Select(_ => GetValue(target))); - } - - if (inpc != null) - { - inputs.Add(WeakObservable.FromEventPattern( - inpc, - nameof(inpc.PropertyChanged)) - .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) - .Select(_ => GetValue(target))); - } - - return Observable.Merge(inputs).StartWith(GetValue(target)); - } - - public bool SetTargetValue(object value, BindingPriority priority) + public override bool SetTargetValue(object value, BindingPriority priority) { var typeInfo = Target.Target.GetType().GetTypeInfo(); var list = Target.Target as IList; @@ -154,9 +126,9 @@ namespace Avalonia.Markup.Data public IList Arguments { get; } - public Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; + public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; - private object GetValue(object target) + protected override object GetValue(object target) { var typeInfo = target.GetType().GetTypeInfo(); var list = target as IList; @@ -309,45 +281,19 @@ namespace Avalonia.Markup.Data } } - private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e) + protected override bool ShouldUpdate(object sender, PropertyChangedEventArgs e) { - if (sender is IList) - { - object indexObject; - - if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject)) - { - return false; - } - - var index = (int)indexObject; - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - return index >= e.NewStartingIndex; - case NotifyCollectionChangedAction.Remove: - return index >= e.OldStartingIndex; - case NotifyCollectionChangedAction.Replace: - return index >= e.NewStartingIndex && - index < e.NewStartingIndex + e.NewItems.Count; - case NotifyCollectionChangedAction.Move: - return (index >= e.NewStartingIndex && - index < e.NewStartingIndex + e.NewItems.Count) || - (index >= e.OldStartingIndex && - index < e.OldStartingIndex + e.OldItems.Count); - case NotifyCollectionChangedAction.Reset: - return true; - } - } - - return true; // Implementation defined meaning for the index, so just try to update anyway + var typeInfo = sender.GetType().GetTypeInfo(); + return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false; } - private bool ShouldUpdate(object sender, PropertyChangedEventArgs e) + protected override int? TryGetFirstArgumentAsInt() { - var typeInfo = sender.GetType().GetTypeInfo(); - return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false; + if (TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out var value)) + { + return (int?)value; + } + return null; } } } diff --git a/src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs b/src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs new file mode 100644 index 0000000000..d3a4a818fe --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/IndexerNodeBase.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reactive.Linq; +using System.Reflection; +using System.Text; +using Avalonia.Data; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Data +{ + abstract class IndexerNodeBase : ExpressionNode, ISettableNode + { + protected override IObservable StartListeningCore(WeakReference reference) + { + var target = reference.Target; + var inputs = new List>(); + + if (target is INotifyCollectionChanged incc) + { + inputs.Add(WeakObservable.FromEventPattern( + incc, + nameof(incc.CollectionChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); + } + + if (target is INotifyPropertyChanged inpc) + { + inputs.Add(WeakObservable.FromEventPattern( + inpc, + nameof(inpc.PropertyChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); + } + + return inputs.Merge().StartWith(GetValue(target)); + } + + public abstract bool SetTargetValue(object value, BindingPriority priority); + + public abstract Type PropertyType { get; } + + protected abstract object GetValue(object target); + + protected abstract int? TryGetFirstArgumentAsInt(); + + private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e) + { + if (sender is IList) + { + var index = TryGetFirstArgumentAsInt(); + + if (index == null) + { + return false; + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + return index >= e.NewStartingIndex; + case NotifyCollectionChangedAction.Remove: + return index >= e.OldStartingIndex; + case NotifyCollectionChangedAction.Replace: + return index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count; + case NotifyCollectionChangedAction.Move: + return (index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count) || + (index >= e.OldStartingIndex && + index < e.OldStartingIndex + e.OldItems.Count); + case NotifyCollectionChangedAction.Reset: + return true; + } + } + + return true; // Implementation defined meaning for the index, so just try to update anyway + } + + protected abstract bool ShouldUpdate(object sender, PropertyChangedEventArgs e); + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs new file mode 100644 index 0000000000..9e225ffcc5 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionTreeParser.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; + +namespace Avalonia.Markup.Data.Parsers +{ + class ExpressionTreeParser + { + private readonly bool enableDataValidation; + + public ExpressionTreeParser(bool enableDataValidation) + { + this.enableDataValidation = enableDataValidation; + } + + public ExpressionNode Parse(Expression expr) + { + var visitor = new ExpressionVisitorNodeBuilder(enableDataValidation); + + visitor.Visit(expr); + + var nodes = visitor.Nodes; + + for (int n = 0; n < nodes.Count - 1; ++n) + { + nodes[n].Next = nodes[n + 1]; + } + + return nodes.FirstOrDefault() ?? new EmptyExpressionNode(); + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs new file mode 100644 index 0000000000..0126d31098 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Parsers/ExpressionVisitorNodeBuilder.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace Avalonia.Markup.Data.Parsers +{ + class ExpressionVisitorNodeBuilder : ExpressionVisitor + { + private static PropertyInfo AvaloniaObjectIndexer; + + private readonly bool enableDataValidation; + + static ExpressionVisitorNodeBuilder() + { + AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) }); + } + + public List Nodes { get; } + + public ExpressionVisitorNodeBuilder(bool enableDataValidation) + { + this.enableDataValidation = enableDataValidation; + Nodes = new List(); + } + + protected override Expression VisitUnary(UnaryExpression node) + { + if (node.NodeType != ExpressionType.Not || node.Type != typeof(bool)) + { + throw new ExpressionParseException(0, $"Invalid unary operation {node.NodeType} in binding expression"); + } + + Nodes.Add(new LogicalNotNode()); + + return base.VisitUnary(node); + } + + protected override Expression VisitMember(MemberExpression node) + { + Nodes.Add(new PropertyAccessorNode(node.Member.Name, enableDataValidation)); + return base.VisitMember(node); + } + + protected override Expression VisitIndex(IndexExpression node) + { + if (node.Indexer == AvaloniaObjectIndexer) + { + var property = GetArgumentExpressionValue(node.Arguments[0]); + Nodes.Add(new PropertyAccessorNode($"{property.OwnerType.Name}.{property.Name}", enableDataValidation)); + } + else + { + Nodes.Add(new IndexerExpressionNode(node)); + } + + return node; + } + + private T GetArgumentExpressionValue(Expression expr) + { + try + { + return Expression.Lambda>(expr).Compile(preferInterpretation: true)(); + } + catch (InvalidOperationException ex) + { + throw new ExpressionParseException(0, "Unable to parse indexer value.", ex); + } + } + + protected override Expression VisitBinary(BinaryExpression node) + { + if (node.NodeType == ExpressionType.ArrayIndex) + { + return base.VisitBinary(node); + } + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitBlock(BlockExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override CatchBlock VisitCatchBlock(CatchBlock node) + { + throw new ExpressionParseException(0, $"Catch blocks are not allowed in binding expressions."); + } + + protected override Expression VisitConditional(ConditionalExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitDynamic(DynamicExpression node) + { + throw new ExpressionParseException(0, $"Dynamic expressions are not allowed in binding expressions."); + } + + protected override ElementInit VisitElementInit(ElementInit node) + { + throw new ExpressionParseException(0, $"Element init expressions are not valid in a binding expression."); + } + + protected override Expression VisitGoto(GotoExpression node) + { + throw new ExpressionParseException(0, $"Goto expressions not supported in binding expressions."); + } + + protected override Expression VisitInvocation(InvocationExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitLabel(LabelExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitListInit(ListInitExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitLoop(LoopExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override MemberAssignment VisitMemberAssignment(MemberAssignment node) + { + throw new ExpressionParseException(0, $"Member assignments not supported in binding expressions."); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitSwitch(SwitchExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitTry(TryExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + + protected override Expression VisitTypeBinary(TypeBinaryExpression node) + { + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs new file mode 100644 index 0000000000..3fdd9ffc10 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_ExpressionTree.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Markup.Data; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.UnitTests.Data +{ + public class ExpressionObserverTests_ExpressionTree + { + [Fact] + public async Task IdentityExpression_Creates_IdentityObserver() + { + var target = new object(); + + var observer = ExpressionObserver.CreateFromExpression(target, o => o); + + Assert.Equal(target, await observer.Take(1)); + } + + [Fact] + public async Task Property_Access_Expression_Observes_Property() + { + var target = new Class1(); + + var observer = ExpressionObserver.CreateFromExpression(target, o => o.Foo); + + Assert.Null(await observer.Take(1)); + + using (observer.Subscribe(_ => {})) + { + target.Foo = "Test"; + } + + Assert.Equal("Test", await observer.Take(1)); + + GC.KeepAlive(target); + } + + [Fact] + public void Property_Acccess_Expression_Can_Set_Property() + { + var data = new Class1(); + var target = ExpressionObserver.CreateFromExpression(data, o => o.Foo); + + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue("baz")); + } + + GC.KeepAlive(data); + } + + [Fact] + public async Task Indexer_Accessor_Can_Read_Value() + { + var data = new[] { 1, 2, 3, 4 }; + + var target = ExpressionObserver.CreateFromExpression(data, o => o[0]); + + Assert.Equal(data[0], await target.Take(1)); + } + + [Fact] + public async Task Indexer_Accessor_Can_Read_Complex_Index() + { + var data = new Dictionary(); + + var key = new object(); + + data.Add(key, new object()); + + var target = ExpressionObserver.CreateFromExpression(data, o => o[key]); + + Assert.Equal(data[key], await target.Take(1)); + } + + [Fact] + public void Indexer_Can_Set_Value() + { + var data = new[] { 1, 2, 3, 4 }; + + var target = ExpressionObserver.CreateFromExpression(data, o => o[0]); + + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue(2)); + } + + GC.KeepAlive(data); + } + + private class Class1 : NotifyingBase + { + private string _foo; + + public string Foo + { + get { return _foo; } + set + { + _foo = value; + RaisePropertyChanged(nameof(Foo)); + } + } + } + } +} From bf6375fe266f3825e59eaf5f020c9bfbe955f45a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 29 May 2018 16:49:08 -0500 Subject: [PATCH 011/211] Fix indexer and casting expressions. --- .../Parsers/ExpressionVisitorNodeBuilder.cs | 49 ++++++++++-- .../ExpressionObserverTests_ExpressionTree.cs | 80 +++++++++++++++++++ 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs index 8564bf5111..5affe227e1 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -28,24 +29,43 @@ namespace Avalonia.Data.Core.Parsers protected override Expression VisitUnary(UnaryExpression node) { - if (node.NodeType != ExpressionType.Not || node.Type != typeof(bool)) + if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool)) { - throw new ExpressionParseException(0, $"Invalid unary operation {node.NodeType} in binding expression"); + Nodes.Add(new LogicalNotNode()); + } + else if (node.NodeType == ExpressionType.Convert) + { + if (node.Operand.Type.IsAssignableFrom(node.Type)) + { + // Ignore inheritance casts + } + else + { + throw new ExpressionParseException(0, $"Cannot parse non-inheritance casts in a binding expression."); + } + } + else if (node.NodeType == ExpressionType.TypeAs) + { + // Ignore as operator. + } + else + { + throw new ExpressionParseException(0, $"Unable to parse unary operator {node.NodeType} in a binding expression"); } - - Nodes.Add(new LogicalNotNode()); return base.VisitUnary(node); } protected override Expression VisitMember(MemberExpression node) { + var visited = base.VisitMember(node); Nodes.Add(new PropertyAccessorNode(node.Member.Name, enableDataValidation)); - return base.VisitMember(node); + return visited; } protected override Expression VisitIndex(IndexExpression node) { + var visited = base.VisitIndex(node); if (node.Indexer == AvaloniaObjectIndexer) { var property = GetArgumentExpressionValue(node.Arguments[0]); @@ -56,7 +76,7 @@ namespace Avalonia.Data.Core.Parsers Nodes.Add(new IndexerExpressionNode(node)); } - return node; + return visited; } private T GetArgumentExpressionValue(Expression expr) @@ -75,7 +95,8 @@ namespace Avalonia.Data.Core.Parsers { if (node.NodeType == ExpressionType.ArrayIndex) { - return base.VisitBinary(node); + base.VisitBinary(node); + return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right })); } throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); } @@ -137,9 +158,23 @@ namespace Avalonia.Data.Core.Parsers protected override Expression VisitMethodCall(MethodCallExpression node) { + base.VisitMethodCall(node); + var property = TryGetPropertyFromMethod(node.Method); + + if (property != null) + { + return Visit(Expression.MakeIndex(node.Object, property, node.Arguments)); + } + throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); } + private PropertyInfo TryGetPropertyFromMethod(MethodInfo method) + { + var type = method.DeclaringType; + return type.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method); + } + protected override Expression VisitSwitch(SwitchExpression node) { throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs index 1ec4bdb4f5..ebf3ca2a49 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_ExpressionTree.cs @@ -19,6 +19,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var observer = ExpressionObserver.CreateFromExpression(target, o => o); Assert.Equal(target, await observer.Take(1)); + GC.KeepAlive(target); } [Fact] @@ -62,6 +63,18 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = ExpressionObserver.CreateFromExpression(data, o => o[0]); Assert.Equal(data[0], await target.Take(1)); + GC.KeepAlive(data); + } + + [Fact] + public async Task Indexer_List_Accessor_Can_Read_Value() + { + var data = new List { 1, 2, 3, 4 }; + + var target = ExpressionObserver.CreateFromExpression(data, o => o[0]); + + Assert.Equal(data[0], await target.Take(1)); + GC.KeepAlive(data); } [Fact] @@ -76,6 +89,8 @@ namespace Avalonia.Base.UnitTests.Data.Core var target = ExpressionObserver.CreateFromExpression(data, o => o[key]); Assert.Equal(data[key], await target.Take(1)); + + GC.KeepAlive(data); } [Fact] @@ -93,6 +108,62 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } + [Fact] + public async Task Inheritance_Casts_Should_Be_Ignored() + { + NotifyingBase test = new Class1 { Foo = "Test" }; + + var target = ExpressionObserver.CreateFromExpression(test, o => ((Class1)o).Foo); + + Assert.Equal("Test", await target.Take(1)); + + GC.KeepAlive(test); + } + + [Fact] + public void Convert_Casts_Should_Error() + { + var test = 1; + + Assert.Throws(() => ExpressionObserver.CreateFromExpression(test, o => (double)o)); + } + + [Fact] + public async Task As_Operator_Should_Be_Ignored() + { + NotifyingBase test = new Class1 { Foo = "Test" }; + + var target = ExpressionObserver.CreateFromExpression(test, o => (o as Class1).Foo); + + Assert.Equal("Test", await target.Take(1)); + + GC.KeepAlive(test); + } + + [Fact] + public async Task Avalonia_Property_Indexer_Reads_Avalonia_Property_Value() + { + var test = new Class2(); + + var target = ExpressionObserver.CreateFromExpression(test, o => o[Class2.FooProperty]); + + Assert.Equal("foo", await target.Take(1)); + + GC.KeepAlive(test); + } + + [Fact] + public async Task Complex_Expression_Correctly_Parsed() + { + var test = new Class1 { Foo = "Test" }; + + var target = ExpressionObserver.CreateFromExpression(test, o => o.Foo.Length); + + Assert.Equal(test.Foo.Length, await target.Take(1)); + + GC.KeepAlive(test); + } + private class Class1 : NotifyingBase { private string _foo; @@ -107,5 +178,14 @@ namespace Avalonia.Base.UnitTests.Data.Core } } } + + + private class Class2 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register("Foo", defaultValue: "foo"); + + public string ClrProperty { get; } = "clr-property"; + } } } From d2823110318865693625463f92eaf42c64ed98c4 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 31 May 2018 18:39:03 -0500 Subject: [PATCH 012/211] Fix bug in ExpressionVisitorNodeBuilder. --- .../Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs index 5affe227e1..433cfd1889 100644 --- a/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs +++ b/src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs @@ -65,7 +65,8 @@ namespace Avalonia.Data.Core.Parsers protected override Expression VisitIndex(IndexExpression node) { - var visited = base.VisitIndex(node); + Visit(node.Object); + if (node.Indexer == AvaloniaObjectIndexer) { var property = GetArgumentExpressionValue(node.Arguments[0]); @@ -76,7 +77,7 @@ namespace Avalonia.Data.Core.Parsers Nodes.Add(new IndexerExpressionNode(node)); } - return visited; + return node; } private T GetArgumentExpressionValue(Expression expr) @@ -158,7 +159,6 @@ namespace Avalonia.Data.Core.Parsers protected override Expression VisitMethodCall(MethodCallExpression node) { - base.VisitMethodCall(node); var property = TryGetPropertyFromMethod(node.Method); if (property != null) From e576ec178c95ccbd8a5772754ad1c30d87465d26 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 6 Jun 2018 21:06:05 +0200 Subject: [PATCH 013/211] Initial --- src/Avalonia.Controls/AppBuilderBase.cs | 45 +++--- src/Avalonia.Controls/Application.cs | 118 ++++++++++++++- src/Avalonia.Controls/ExitMode.cs | 12 ++ src/Avalonia.Controls/Window.cs | 51 ++++--- src/Avalonia.Controls/WindowCollection.cs | 134 ++++++++++++++++++ .../ApplicationTests.cs | 107 ++++++++++++++ .../WindowTests.cs | 10 +- 7 files changed, 428 insertions(+), 49 deletions(-) create mode 100644 src/Avalonia.Controls/ExitMode.cs create mode 100644 src/Avalonia.Controls/WindowCollection.cs create mode 100644 tests/Avalonia.Controls.UnitTests/ApplicationTests.cs diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 7af3deef34..875f5263c2 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls public abstract class AppBuilderBase where TAppBuilder : AppBuilderBase, new() { private static bool s_setupWasAlreadyCalled; - + /// /// Gets or sets the instance. /// @@ -92,7 +92,7 @@ namespace Avalonia.Controls }; } - protected TAppBuilder Self => (TAppBuilder) this; + protected TAppBuilder Self => (TAppBuilder)this; /// /// Registers a callback to call before Start is called on the . @@ -125,7 +125,6 @@ namespace Avalonia.Controls var window = new TMainWindow(); if (dataContextProvider != null) window.DataContext = dataContextProvider(); - window.Show(); Instance.Run(window); } @@ -143,7 +142,6 @@ namespace Avalonia.Controls if (dataContextProvider != null) mainWindow.DataContext = dataContextProvider(); - mainWindow.Show(); Instance.Run(mainWindow); } @@ -209,6 +207,17 @@ namespace Avalonia.Controls public TAppBuilder UseAvaloniaModules() => AfterSetup(builder => SetupAvaloniaModules()); + /// + /// Sets the shutdown mode of the application. + /// + /// The shutdown mode. + /// + public TAppBuilder SetExitMode(ExitMode exitMode) + { + Instance.ExitMode = exitMode; + return Self; + } + private bool CheckSetup { get; set; } = true; /// @@ -223,20 +232,20 @@ namespace Avalonia.Controls private void SetupAvaloniaModules() { var moduleInitializers = from assembly in AvaloniaLocator.Current.GetService().GetLoadedAssemblies() - from attribute in assembly.GetCustomAttributes() - where attribute.ForWindowingSubsystem == "" - || attribute.ForWindowingSubsystem == WindowingSubsystemName - where attribute.ForRenderingSubsystem == "" - || attribute.ForRenderingSubsystem == RenderingSubsystemName - group attribute by attribute.Name into exports - select (from export in exports - orderby export.ForWindowingSubsystem.Length descending - orderby export.ForRenderingSubsystem.Length descending - select export).First().ModuleType into moduleType - select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors - where constructor.GetParameters().Length == 0 && !constructor.IsStatic - select constructor).Single() into constructor - select (Action)(() => constructor.Invoke(new object[0])); + from attribute in assembly.GetCustomAttributes() + where attribute.ForWindowingSubsystem == "" + || attribute.ForWindowingSubsystem == WindowingSubsystemName + where attribute.ForRenderingSubsystem == "" + || attribute.ForRenderingSubsystem == RenderingSubsystemName + group attribute by attribute.Name into exports + select (from export in exports + orderby export.ForWindowingSubsystem.Length descending + orderby export.ForRenderingSubsystem.Length descending + select export).First().ModuleType into moduleType + select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors + where constructor.GetParameters().Length == 0 && !constructor.IsStatic + select constructor).Single() into constructor + select (Action)(() => constructor.Invoke(new object[0])); Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke(); } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 6fdca557eb..ffe4a9c513 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -43,11 +43,15 @@ namespace Avalonia private Styles _styles; private IResourceDictionary _resources; + private CancellationTokenSource _mainLoopCancellationTokenSource; + /// /// Initializes a new instance of the class. /// public Application() { + Windows = new WindowCollection(this); + OnExit += OnExiting; } @@ -158,6 +162,40 @@ namespace Avalonia /// IResourceNode IResourceNode.ResourceParent => null; + /// + /// Gets or sets the . This property indicates whether the application exits explicitly or implicitly. + /// If is set to OnExplicitExit the application is only closes if Exit is called. + /// The default is OnLastWindowClose + /// + /// + /// The shutdown mode. + /// + public ExitMode ExitMode { get; set; } + + /// + /// Gets or sets the main window of the application. + /// + /// + /// The main window. + /// + public Window MainWindow { get; set; } + + /// + /// Gets the open windows of the application. + /// + /// + /// The windows. + /// + public WindowCollection Windows { get; } + + /// + /// Gets or sets a value indicating whether this instance is existing. + /// + /// + /// true if this instance is existing; otherwise, false. + /// + internal bool IsExiting { get; set; } + /// /// Initializes the application by loading XAML etc. /// @@ -171,19 +209,81 @@ namespace Avalonia /// The closable to track public void Run(ICloseable closable) { - var source = new CancellationTokenSource(); - closable.Closed += OnExiting; - closable.Closed += (s, e) => source.Cancel(); - Dispatcher.UIThread.MainLoop(source.Token); + if (_mainLoopCancellationTokenSource != null) + { + throw new Exception("Run should only called once"); + } + + closable.Closed += (s, e) => Exit(); + + _mainLoopCancellationTokenSource = new CancellationTokenSource(); + + Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); + + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } + } + + /// + /// Runs the application's main loop until some condition occurs that is specified by ExitMode. + /// + /// The main window + public void Run(Window mainWindow) + { + if (_mainLoopCancellationTokenSource != null) + { + throw new Exception("Run should only called once"); + } + + _mainLoopCancellationTokenSource = new CancellationTokenSource(); + + Dispatcher.UIThread.InvokeAsync( + () => + { + if (mainWindow == null) + { + return; + } + + if (MainWindow != null) + { + return; + } + + if (!mainWindow.IsVisible) + { + mainWindow.Show(); + } + + MainWindow = mainWindow; + }, + DispatcherPriority.Send); + + Dispatcher.UIThread.MainLoop(_mainLoopCancellationTokenSource.Token); + + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } - + /// - /// Runs the application's main loop until the is cancelled. + /// Runs the application's main loop until the is canceled. /// /// The token to track public void Run(CancellationToken token) { Dispatcher.UIThread.MainLoop(token); + + // Make sure we call OnExit in case an error happened and Exit() wasn't called explicitly + if (!IsExiting) + { + OnExit?.Invoke(this, EventArgs.Empty); + } } /// @@ -191,7 +291,13 @@ namespace Avalonia /// public void Exit() { + IsExiting = true; + + Windows.Clear(); + OnExit?.Invoke(this, EventArgs.Empty); + + _mainLoopCancellationTokenSource?.Cancel(); } /// diff --git a/src/Avalonia.Controls/ExitMode.cs b/src/Avalonia.Controls/ExitMode.cs new file mode 100644 index 0000000000..0c5ecd7171 --- /dev/null +++ b/src/Avalonia.Controls/ExitMode.cs @@ -0,0 +1,12 @@ +// 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 +{ + public enum ExitMode + { + OnLastWindowClose, + OnMainWindowClose, + OnExplicitExit + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 3cbfdbd657..c19c69ce73 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -49,14 +49,6 @@ namespace Avalonia.Controls /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope { - private static List s_windows = new List(); - - /// - /// Retrieves an enumeration of all Windows in the currently running application. - /// - public static IReadOnlyList OpenWindows => s_windows; - - /// /// Defines the property. /// public static readonly StyledProperty SizeToContentProperty = @@ -75,7 +67,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(ShowInTaskbar), true); /// - /// Enables or disables the taskbar icon + /// Represents the current window state (normal, minimized, maximized) /// public static readonly StyledProperty WindowStateProperty = AvaloniaProperty.Register(nameof(WindowState)); @@ -117,7 +109,7 @@ namespace Avalonia.Controls BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue)); HasSystemDecorationsProperty.Changed.AddClassHandler( - (s, e) => s.PlatformImpl?.SetSystemDecorations((bool) e.NewValue)); + (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue)); ShowInTaskbarProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue)); @@ -149,7 +141,7 @@ namespace Avalonia.Controls _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); Screens = new Screens(PlatformImpl?.Screen); } - + /// event EventHandler INameScope.Registered { @@ -199,7 +191,7 @@ namespace Avalonia.Controls get { return GetValue(HasSystemDecorationsProperty); } set { SetValue(HasSystemDecorationsProperty, value); } } - + /// /// Enables or disables the taskbar icon /// @@ -259,6 +251,26 @@ namespace Avalonia.Controls /// public event EventHandler Closing; + private static void AddWindow(Window window) + { + if (Application.Current == null) + { + return; + } + + Application.Current.Windows.Add(window); + } + + private static void RemoveWindow(Window window) + { + if (Application.Current == null) + { + return; + } + + Application.Current.Windows.Remove(window); + } + /// /// Closes the window. /// @@ -298,10 +310,9 @@ namespace Avalonia.Controls finally { if (ignoreCancel || !cancelClosing) - { - s_windows.Remove(this); + { PlatformImpl?.Dispose(); - IsVisible = false; + HandleClosed(); } } } @@ -359,7 +370,7 @@ namespace Avalonia.Controls return; } - s_windows.Add(this); + AddWindow(this); EnsureInitialized(); SetWindowStartupLocation(); @@ -400,7 +411,7 @@ namespace Avalonia.Controls throw new InvalidOperationException("The window is already being shown."); } - s_windows.Add(this); + AddWindow(this); EnsureInitialized(); SetWindowStartupLocation(); @@ -409,7 +420,7 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { - var affectedWindows = s_windows.Where(w => w.IsEnabled && w != this).ToList(); + var affectedWindows = Application.Current.Windows.Where(w => w.IsEnabled && w != this).ToList(); var activated = affectedWindows.Where(w => w.IsActive).FirstOrDefault(); SetIsEnabled(affectedWindows, false); @@ -513,8 +524,8 @@ namespace Avalonia.Controls protected override void HandleClosed() { - IsVisible = false; - s_windows.Remove(this); + RemoveWindow(this); + base.HandleClosed(); } diff --git a/src/Avalonia.Controls/WindowCollection.cs b/src/Avalonia.Controls/WindowCollection.cs new file mode 100644 index 0000000000..c21a12f05b --- /dev/null +++ b/src/Avalonia.Controls/WindowCollection.cs @@ -0,0 +1,134 @@ +// 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; +using System.Collections.Generic; + +using Avalonia.Controls; + +namespace Avalonia +{ + public class WindowCollection : IReadOnlyList + { + private readonly Application _application; + private readonly List _windows = new List(); + + public WindowCollection(Application application) + { + _application = application; + } + + /// + /// + /// Gets the number of elements in the collection. + /// + public int Count => _windows.Count; + + /// + /// + /// Gets the at the specified index. + /// + /// + /// The . + /// + /// The index. + /// + public Window this[int index] => _windows[index]; + + /// + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// An enumerator that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() + { + return _windows.GetEnumerator(); + } + + /// + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Adds the specified window. + /// + /// The window. + internal void Add(Window window) + { + if (window == null) + { + return; + } + + _windows.Add(window); + } + + /// + /// Removes the specified window. + /// + /// The window. + internal void Remove(Window window) + { + if (window == null) + { + return; + } + + _windows.Remove(window); + + OnRemoveWindow(window); + } + + /// + /// Closes all windows and removes them from the underlying collection. + /// + internal void Clear() + { + while (_windows.Count > 0) + { + _windows[0].Close(); + } + } + + private void OnRemoveWindow(Window window) + { + if (window == null) + { + return; + } + + if (_application.IsExiting) + { + return; + } + + switch (_application.ExitMode) + { + case ExitMode.OnLastWindowClose: + if (Count == 0) + { + _application.Exit(); + } + + break; + case ExitMode.OnMainWindowClose: + if (window == _application.MainWindow) + { + _application.Exit(); + } + + break; + } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs new file mode 100644 index 0000000000..85f95b2b5c --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs @@ -0,0 +1,107 @@ +// 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 Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ApplicationTests + { + [Fact] + public void Should_Exit_After_MainWindow_Closed() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnMainWindowClose; + + var mainWindow = new Window(); + + mainWindow.Show(); + + Application.Current.MainWindow = mainWindow; + + var window = new Window(); + + window.Show(); + + mainWindow.Close(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Exit_After_Last_Window_Closed() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnLastWindowClose; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + windowA.Close(); + + Assert.False(Application.Current.IsExiting); + + windowB.Close(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Only_Exit_On_Explicit_Exit() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.ExitMode = ExitMode.OnExplicitExit; + + var windowA = new Window(); + + windowA.Show(); + + var windowB = new Window(); + + windowB.Show(); + + windowA.Close(); + + Assert.False(Application.Current.IsExiting); + + windowB.Close(); + + Assert.False(Application.Current.IsExiting); + + Application.Current.Exit(); + + Assert.True(Application.Current.IsExiting); + } + } + + [Fact] + public void Should_Close_All_Remaining_Open_Windows_After_Explicit_Exit_Call() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var windows = new List { new Window(), new Window(), new Window(), new Window() }; + + foreach (var window in windows) + { + window.Show(); + } + + Application.Current.Exit(); + + Assert.Empty(Application.Current.Windows); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index a85c4df8af..e80ffd97cd 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -129,7 +129,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); - Assert.Equal(new[] { window }, Window.OpenWindows); + Assert.Equal(new[] { window }, Application.Current.Windows); } } @@ -145,7 +145,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); window.IsVisible = true; - Assert.Equal(new[] { window }, Window.OpenWindows); + Assert.Equal(new[] { window }, Application.Current.Windows); window.Close(); } @@ -162,7 +162,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); window.Close(); - Assert.Empty(Window.OpenWindows); + Assert.Empty(Application.Current.Windows); } } @@ -184,7 +184,7 @@ namespace Avalonia.Controls.UnitTests window.Show(); windowImpl.Object.Closed(); - Assert.Empty(Window.OpenWindows); + Assert.Empty(Application.Current.Windows); } } @@ -339,7 +339,7 @@ namespace Avalonia.Controls.UnitTests { // HACK: We really need a decent way to have "statics" that can be scoped to // AvaloniaLocator scopes. - ((IList)Window.OpenWindows).Clear(); + Application.Current.Windows.Clear(); } } } From 4f26d4fc0865c71a346f526dd13b300df546aa63 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 7 Jun 2018 15:32:57 +0100 Subject: [PATCH 014/211] dont highlight selected menuitems. --- src/Avalonia.Themes.Default/MenuItem.xaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index efb31175fa..319ff189f0 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -122,11 +122,6 @@ - - - \ No newline at end of file + + + From dcafd6e764294dba98435345730ed479691cbd16 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 19 Jul 2018 23:20:18 -0500 Subject: [PATCH 163/211] Use transforms instead of Canvas to support the ProgressBar indeterminate animation. --- src/Avalonia.Controls/ProgressBar.cs | 14 ++++++++++++- src/Avalonia.Themes.Default/ProgressBar.xaml | 22 ++++++++++---------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index af152cf247..a42d793ff1 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -24,6 +24,9 @@ namespace Avalonia.Controls private static readonly StyledProperty IndeterminateStartingOffsetProperty = AvaloniaProperty.Register(nameof(IndeterminateStartingOffset)); + private static readonly StyledProperty IndeterminateEndingOffsetProperty = + AvaloniaProperty.Register(nameof(IndeterminateEndingOffset)); + private Border _indicator; static ProgressBar() @@ -53,6 +56,12 @@ namespace Avalonia.Controls set => SetValue(IndeterminateStartingOffsetProperty, value); } + private double IndeterminateEndingOffset + { + get => GetValue(IndeterminateEndingOffsetProperty); + set => SetValue(IndeterminateEndingOffsetProperty, value); + } + /// protected override Size ArrangeOverride(Size finalSize) { @@ -79,12 +88,15 @@ namespace Avalonia.Controls var width = bounds.Width / 5.0; IndeterminateStartingOffset = -width; _indicator.Width = width; + IndeterminateEndingOffset = bounds.Width; + } else { var height = bounds.Height / 5.0; - IndeterminateStartingOffset = -height; + IndeterminateStartingOffset = -bounds.Height; _indicator.Height = height; + IndeterminateEndingOffset = height; } } else diff --git a/src/Avalonia.Themes.Default/ProgressBar.xaml b/src/Avalonia.Themes.Default/ProgressBar.xaml index df735e2048..c4cbfed350 100644 --- a/src/Avalonia.Themes.Default/ProgressBar.xaml +++ b/src/Avalonia.Themes.Default/ProgressBar.xaml @@ -7,11 +7,9 @@ - - - + @@ -19,12 +17,10 @@ + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = (TextBlock)window.Content; + + window.DataContext = 5.6; + window.ApplyTemplate(); + + Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock)); + } + } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 2c7e850fee..4517aa6aa1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -146,5 +146,31 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.NotNull(target.FocusAdorner); } } + + [Fact] + public void Setter_Can_Set_Attached_Property() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = (TextBlock)window.Content; + + window.ApplyTemplate(); + + Assert.Equal(Dock.Right, DockPanel.GetDock(textBlock)); + } + } } -} \ No newline at end of file +} From ffcaa545bb64db5489de3cb135e4f831afb07a8f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 10:45:07 +0200 Subject: [PATCH 171/211] Added PropertyParser. So we don't need to use a regex to parse property strings. --- .../Avalonia.Markup.Xaml.csproj | 1 + .../Parsers/PropertyParser.cs | 84 +++++++ .../Markup/Parsers/IdentifierParser.cs | 2 +- .../Avalonia.Markup/Markup/Parsers/Reader.cs | 2 +- .../Parsers/PropertyParserTests.cs | 225 ++++++++++++++++++ 5 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index cdc22f4102..8c843a4b49 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs new file mode 100644 index 0000000000..ce82ffe0a1 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs @@ -0,0 +1,84 @@ +using System; +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; + +namespace Avalonia.Markup.Xaml.Parsers +{ + internal class PropertyParser + { + public (string ns, string owner, string name) Parse(Reader r) + { + if (r.End) + { + throw new ExpressionParseException(0, "Expected property name."); + } + + var openParens = r.TakeIf('('); + bool closeParens = false; + string ns = null; + string owner = null; + string name = null; + + do + { + var token = IdentifierParser.Parse(r); + + if (token == null) + { + if (r.End) + { + break; + } + else + { + if (openParens && !r.End && (closeParens = r.TakeIf(')'))) + { + break; + } + else if (openParens) + { + throw new ExpressionParseException(r.Position, $"Expected ')'."); + } + + throw new ExpressionParseException(r.Position, $"Unexpected '{r.Peek}'."); + } + } + else if (!r.End && r.TakeIf(':')) + { + ns = ns == null ? + token : + throw new ExpressionParseException(r.Position, "Unexpected ':'."); + } + else if (!r.End && r.TakeIf('.')) + { + owner = owner == null ? + token : + throw new ExpressionParseException(r.Position, "Unexpected '.'."); + } + else + { + name = token; + } + } while (!r.End); + + if (name == null) + { + throw new ExpressionParseException(0, "Expected property name."); + } + else if (openParens && owner == null) + { + throw new ExpressionParseException(1, "Expected property owner."); + } + else if (openParens && !closeParens) + { + throw new ExpressionParseException(r.Position, "Expected ')'."); + } + else if (!r.End) + { + throw new ExpressionParseException(r.Position, "Expected end of expression."); + } + + return (ns, owner, name); + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs index f86f2db321..9431dab45e 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs @@ -6,7 +6,7 @@ using System.Text; namespace Avalonia.Markup.Parsers { - internal static class IdentifierParser + public static class IdentifierParser { public static string Parse(Reader r) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs index 9355bc9aa3..4a3d6aa277 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs @@ -5,7 +5,7 @@ using System; namespace Avalonia.Markup.Parsers { - internal class Reader + public class Reader { private readonly string _s; private int _i; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs new file mode 100644 index 0000000000..a05485f55b --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs @@ -0,0 +1,225 @@ +using System; +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.Parsers; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Parsers +{ + public class PropertyParserTests + { + [Fact] + public void Parses_Name() + { + var target = new PropertyParser(); + var reader = new Reader("Foo"); + var (ns, owner, name) = target.Parse(reader); + + Assert.Null(ns); + Assert.Null(owner); + Assert.Equal("Foo", name); + } + + [Fact] + public void Parses_Owner_And_Name() + { + var target = new PropertyParser(); + var reader = new Reader("Foo.Bar"); + var (ns, owner, name) = target.Parse(reader); + + Assert.Null(ns); + Assert.Equal("Foo", owner); + Assert.Equal("Bar", name); + } + + [Fact] + public void Parses_Namespace_Owner_And_Name() + { + var target = new PropertyParser(); + var reader = new Reader("foo:Bar.Baz"); + var (ns, owner, name) = target.Parse(reader); + + Assert.Equal("foo", ns); + Assert.Equal("Bar", owner); + Assert.Equal("Baz", name); + } + + [Fact] + public void Parses_Owner_And_Name_With_Parentheses() + { + var target = new PropertyParser(); + var reader = new Reader("(Foo.Bar)"); + var (ns, owner, name) = target.Parse(reader); + + Assert.Null(ns); + Assert.Equal("Foo", owner); + Assert.Equal("Bar", name); + } + + [Fact] + public void Parses_Namespace_Owner_And_Name_With_Parentheses() + { + var target = new PropertyParser(); + var reader = new Reader("(foo:Bar.Baz)"); + var (ns, owner, name) = target.Parse(reader); + + Assert.Equal("foo", ns); + Assert.Equal("Bar", owner); + Assert.Equal("Baz", name); + } + + [Fact] + public void Fails_With_Empty_String() + { + var target = new PropertyParser(); + var reader = new Reader(""); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(0, ex.Column); + Assert.Equal("Expected property name.", ex.Message); + } + + [Fact] + public void Fails_With_Only_Whitespace() + { + var target = new PropertyParser(); + var reader = new Reader(" "); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(0, ex.Column); + Assert.Equal("Unexpected ' '.", ex.Message); + } + + [Fact] + public void Fails_With_Leading_Whitespace() + { + var target = new PropertyParser(); + var reader = new Reader(" Foo"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(0, ex.Column); + Assert.Equal("Unexpected ' '.", ex.Message); + } + + [Fact] + public void Fails_With_Trailing_Whitespace() + { + var target = new PropertyParser(); + var reader = new Reader("Foo "); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(3, ex.Column); + Assert.Equal("Unexpected ' '.", ex.Message); + } + + [Fact] + public void Fails_With_Invalid_Property_Name() + { + var target = new PropertyParser(); + var reader = new Reader("123"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(0, ex.Column); + Assert.Equal("Unexpected '1'.", ex.Message); + } + + [Fact] + public void Fails_With_Trailing_Junk() + { + var target = new PropertyParser(); + var reader = new Reader("Foo%"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(3, ex.Column); + Assert.Equal("Unexpected '%'.", ex.Message); + } + + [Fact] + public void Fails_With_Invalid_Property_Name_After_Owner() + { + var target = new PropertyParser(); + var reader = new Reader("Foo.123"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(4, ex.Column); + Assert.Equal("Unexpected '1'.", ex.Message); + } + + [Fact] + public void Fails_With_Whitespace_Between_Owner_And_Name() + { + var target = new PropertyParser(); + var reader = new Reader("Foo. Bar"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(4, ex.Column); + Assert.Equal("Unexpected ' '.", ex.Message); + } + + [Fact] + public void Fails_With_Too_Many_Segments() + { + var target = new PropertyParser(); + var reader = new Reader("Foo.Bar.Baz"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(8, ex.Column); + Assert.Equal("Unexpected '.'.", ex.Message); + } + + [Fact] + public void Fails_With_Too_Many_Namespaces() + { + var target = new PropertyParser(); + var reader = new Reader("foo:bar:Baz"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(8, ex.Column); + Assert.Equal("Unexpected ':'.", ex.Message); + } + + [Fact] + public void Fails_With_Parens_But_No_Owner() + { + var target = new PropertyParser(); + var reader = new Reader("(Foo)"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(1, ex.Column); + Assert.Equal("Expected property owner.", ex.Message); + } + + [Fact] + public void Fails_With_Parens_And_Namespace_But_No_Owner() + { + var target = new PropertyParser(); + var reader = new Reader("(foo:Bar)"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(1, ex.Column); + Assert.Equal("Expected property owner.", ex.Message); + } + + [Fact] + public void Fails_With_Missing_Close_Parens() + { + var target = new PropertyParser(); + var reader = new Reader("(Foo.Bar"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(8, ex.Column); + Assert.Equal("Expected ')'.", ex.Message); + } + + [Fact] + public void Fails_With_Unexpected_Close_Parens() + { + var target = new PropertyParser(); + var reader = new Reader("Foo.Bar)"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(7, ex.Column); + Assert.Equal("Unexpected ')'.", ex.Message); + } + } +} From 0b796adc53f923208b41991b6806141492dac8a2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 10:57:43 +0200 Subject: [PATCH 172/211] Fix a few issues with AvaloniaPropertyTypeConverter - Don't use regex to parse property strings, use `PropertyParser` - Handle XAML namespaces on attached properties Fixes #1764 --- .../AvaloniaPropertyTypeConverter.cs | 37 ++++++------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index 63b7811dbc..6cdf0452d0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -4,19 +4,16 @@ using System; using System.ComponentModel; using System.Globalization; -using System.Text.RegularExpressions; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.Templates; using Avalonia.Styling; -using Portable.Xaml; using Portable.Xaml.ComponentModel; -using Portable.Xaml.Markup; namespace Avalonia.Markup.Xaml.Converters { public class AvaloniaPropertyTypeConverter : TypeConverter { - private static readonly Regex regex = new Regex(@"^\(?(\w*)\.(\w*)\)?|(.*)$"); - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { return sourceType == typeof(string); @@ -24,8 +21,10 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - var (owner, propertyName) = ParseProperty((string)value); - var ownerType = TryResolveOwnerByName(context, owner) ?? + var parser = new PropertyParser(); + var reader = new Reader((string)value); + var (ns, owner, propertyName) = parser.Parse(reader); + var ownerType = TryResolveOwnerByName(context, ns, owner) ?? context.GetFirstAmbientValue()?.TargetType ?? context.GetFirstAmbientValue + + +"; + var loader = new AvaloniaXamlLoader(); + var ex = Assert.Throws(() => loader.Load(xaml)); + + Assert.Equal( + "Property 'Button.IsDefault' is not registered on 'Avalonia.Controls.TextBlock'.", + ex.InnerException.Message); + } + } } } From 83347b453f062fe7ce378a2b7b974460784f1835 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 21 Jul 2018 02:37:08 +0800 Subject: [PATCH 174/211] Add Animation.Done event and a awaitable Run() method. --- src/Avalonia.Animation/Animation.cs | 38 +++++++++++++++++-- .../AnimatorStateMachine`1.cs | 3 ++ src/Avalonia.Animation/IAnimation.cs | 10 ++++- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 4e777b36ed..46b638dcf4 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -10,6 +10,8 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Reflection; using System.Linq; +using System.Threading.Tasks; +using System.Reactive.Linq; namespace Avalonia.Animation { @@ -24,7 +26,7 @@ namespace Avalonia.Animation }; public static void RegisterAnimator(Func condition) - where TAnimator: IAnimator + where TAnimator : IAnimator { Animators.Insert(0, (condition, typeof(TAnimator))); } @@ -72,14 +74,19 @@ namespace Avalonia.Animation /// /// Easing function to be used. - /// + /// public Easing Easing { get; set; } = new LinearEasing(); + /// + /// Triggers when the animation is completed. + /// + public event EventHandler Done; + public Animation() { this.CollectionChanged += delegate { _isChildrenChanged = true; }; } - + private IList InterpretKeyframes(Animatable control) { var handlerList = new List<(Type type, AvaloniaProperty property)>(); @@ -153,5 +160,28 @@ namespace Avalonia.Animation } return this; } + + /// + public Task Run(Animatable control) + { + var tcs = new TaskCompletionSource(); + + if (this.RepeatCount == RepeatCount.Loop) + tcs.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); + + this.Done += delegate + { + tcs.SetResult(null); + } + + this.Apply(control, Observable.Return(true)); + + return tcs.Task; + } + + internal void SetDone(Animatable control) + { + Done.Invoke(control, null); + } } -} +} \ No newline at end of file diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 1a51b897c0..b5138ad26b 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -243,7 +243,10 @@ namespace Avalonia.Animation { _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); } + _targetObserver.OnCompleted(); + _targetAnimation.SetDone(_targetControl); + handled = true; break; default: diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index 4de7e46af5..7ceb6d5fe4 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; namespace Avalonia.Animation { @@ -13,5 +14,10 @@ namespace Avalonia.Animation /// Apply the animation to the specified control /// IDisposable Apply(Animatable control, IObservable match); + + /// + /// Run the animation to the specified control + /// + Task Run(Animatable control); } -} +} \ No newline at end of file From e0be7353a88e37957173a2fd19df643b206eb4c2 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 20 Jul 2018 14:02:43 -0500 Subject: [PATCH 175/211] Fix tests that broke from the API change. --- .../Parsers/ExpressionNodeBuilderTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs index 212b16965c..2d687ff4f7 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs @@ -162,8 +162,9 @@ namespace Avalonia.Markup.UnitTests.Parsers Assert.Equal(e.Arguments.ToArray(), args); } - private List ToList(ExpressionNode node) + private List ToList((ExpressionNode node, SourceMode mode) parsed) { + var (node, _) = parsed; var result = new List(); while (node != null) From 62526bef3599681e08d9b6a2ed51d8b9316f0165 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 21:38:14 +0200 Subject: [PATCH 176/211] Don't disallow setting unregistered properties. The changes made to the animation system in #1768 currently needs to be able to set any property on any object in order for animations on transforms to work. --- .../Converters/AvaloniaPropertyTypeConverter.cs | 9 ++++++++- tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index 2470778685..0588e82901 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -5,6 +5,7 @@ using System; using System.ComponentModel; using System.Globalization; using Avalonia.Controls; +using Avalonia.Logging; using Avalonia.Markup.Parsers; using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.Templates; @@ -42,7 +43,13 @@ namespace Avalonia.Markup.Xaml.Converters !property.IsAttached && !registry.IsRegistered(targetType, property)) { - throw new XamlLoadException($"Property '{effectiveOwner.Name}.{propertyName}' is not registered on '{targetType}'."); + Logger.Warning( + LogArea.Property, + this, + "Property '{Owner}.{Name}' is not registered on '{Type}'.", + effectiveOwner, + propertyName, + targetType); } return property; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 1d2f90d383..beaf7477d0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -174,7 +174,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } - [Fact] + [Fact(Skip = "The animation system currently needs to be able to set any property on any object")] public void Disallows_Setting_Non_Registered_Property() { using (UnitTestApplication.Start(TestServices.StyledWindow)) From dc6f14e77550bbf898d48a93f92ac4f1b1fafb8c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 23:04:28 +0200 Subject: [PATCH 177/211] Reader -> CharacterReader And moved `CharacterReader` and `IdentifierParser` into Avalonia.Utilities. --- .../Utilities/CharacterReader.cs} | 6 +-- .../Utilities}/IdentifierParser.cs | 4 +- .../AvaloniaPropertyTypeConverter.cs | 3 +- .../Parsers/PropertyParser.cs | 3 +- .../Markup/Parsers/ArgumentListParser.cs | 3 +- .../Parsers/ExpressionObserverBuilder.cs | 3 +- .../Markup/Parsers/ExpressionParser.cs | 23 +++++------ .../Parsers/PropertyParserTests.cs | 39 ++++++++++--------- 8 files changed, 45 insertions(+), 39 deletions(-) rename src/{Markup/Avalonia.Markup/Markup/Parsers/Reader.cs => Avalonia.Base/Utilities/CharacterReader.cs} (89%) rename src/{Markup/Avalonia.Markup/Markup/Parsers => Avalonia.Base/Utilities}/IdentifierParser.cs (94%) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs similarity index 89% rename from src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs rename to src/Avalonia.Base/Utilities/CharacterReader.cs index 4a3d6aa277..0910d5b969 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -3,14 +3,14 @@ using System; -namespace Avalonia.Markup.Parsers +namespace Avalonia.Utilities { - public class Reader + public class CharacterReader { private readonly string _s; private int _i; - public Reader(string s) + public CharacterReader(string s) { _s = s; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs similarity index 94% rename from src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs rename to src/Avalonia.Base/Utilities/IdentifierParser.cs index 9431dab45e..14b8affbdd 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -4,11 +4,11 @@ using System.Globalization; using System.Text; -namespace Avalonia.Markup.Parsers +namespace Avalonia.Utilities { public static class IdentifierParser { - public static string Parse(Reader r) + public static string Parse(CharacterReader r) { if (IsValidIdentifierStart(r.Peek)) { diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index 0588e82901..627a646bcf 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -10,6 +10,7 @@ using Avalonia.Markup.Parsers; using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.Templates; using Avalonia.Styling; +using Avalonia.Utilities; using Portable.Xaml.ComponentModel; namespace Avalonia.Markup.Xaml.Converters @@ -25,7 +26,7 @@ namespace Avalonia.Markup.Xaml.Converters { var registry = AvaloniaPropertyRegistry.Instance; var parser = new PropertyParser(); - var reader = new Reader((string)value); + var reader = new CharacterReader((string)value); var (ns, owner, propertyName) = parser.Parse(reader); var ownerType = TryResolveOwnerByName(context, ns, owner); var targetType = context.GetFirstAmbientValue()?.TargetType ?? diff --git a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs index ce82ffe0a1..702758efae 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs @@ -1,12 +1,13 @@ using System; using Avalonia.Data.Core; using Avalonia.Markup.Parsers; +using Avalonia.Utilities; namespace Avalonia.Markup.Xaml.Parsers { internal class PropertyParser { - public (string ns, string owner, string name) Parse(Reader r) + public (string ns, string owner, string name) Parse(CharacterReader r) { if (r.End) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index ae48657c01..89ef5dcabc 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Data.Core; +using Avalonia.Utilities; using System; using System.Collections.Generic; using System.Text; @@ -10,7 +11,7 @@ namespace Avalonia.Markup.Parsers { internal static class ArgumentListParser { - public static IList Parse(Reader r, char open, char close) + public static IList Parse(CharacterReader r, char open, char close) { if (r.Peek == open) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index ddbe252fc0..6cfb86634b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -1,4 +1,5 @@ using Avalonia.Data.Core; +using Avalonia.Utilities; using System; using System.Collections.Generic; using System.Reactive; @@ -15,7 +16,7 @@ namespace Avalonia.Markup.Parsers return new EmptyExpressionNode(); } - var reader = new Reader(expression); + var reader = new CharacterReader(expression); var parser = new ExpressionParser(enableValidation, typeResolver); var node = parser.Parse(reader); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 95bb421777..682be572da 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -3,6 +3,7 @@ using Avalonia.Data.Core; using Avalonia.Markup.Parsers.Nodes; +using Avalonia.Utilities; using System; using System.Collections.Generic; using System.Linq; @@ -20,7 +21,7 @@ namespace Avalonia.Markup.Parsers _enableValidation = enableValidation; } - public ExpressionNode Parse(Reader r) + public ExpressionNode Parse(CharacterReader r) { var nodes = new List(); var state = State.Start; @@ -64,7 +65,7 @@ namespace Avalonia.Markup.Parsers return nodes.FirstOrDefault(); } - private State ParseStart(Reader r, IList nodes) + private State ParseStart(CharacterReader r, IList nodes) { if (ParseNot(r)) { @@ -93,7 +94,7 @@ namespace Avalonia.Markup.Parsers return State.End; } - private static State ParseAfterMember(Reader r, IList nodes) + private static State ParseAfterMember(CharacterReader r, IList nodes) { if (ParseMemberAccessor(r)) { @@ -112,7 +113,7 @@ namespace Avalonia.Markup.Parsers return State.End; } - private State ParseBeforeMember(Reader r, IList nodes) + private State ParseBeforeMember(CharacterReader r, IList nodes) { if (ParseOpenBrace(r)) { @@ -132,7 +133,7 @@ namespace Avalonia.Markup.Parsers } } - private State ParseAttachedProperty(Reader r, List nodes) + private State ParseAttachedProperty(CharacterReader r, List nodes) { string ns = string.Empty; string owner; @@ -171,7 +172,7 @@ namespace Avalonia.Markup.Parsers return State.AfterMember; } - private State ParseIndexer(Reader r, List nodes) + private State ParseIndexer(CharacterReader r, List nodes) { var args = ArgumentListParser.Parse(r, '[', ']'); @@ -184,27 +185,27 @@ namespace Avalonia.Markup.Parsers return State.AfterMember; } - private static bool ParseNot(Reader r) + private static bool ParseNot(CharacterReader r) { return !r.End && r.TakeIf('!'); } - private static bool ParseMemberAccessor(Reader r) + private static bool ParseMemberAccessor(CharacterReader r) { return !r.End && r.TakeIf('.'); } - private static bool ParseOpenBrace(Reader r) + private static bool ParseOpenBrace(CharacterReader r) { return !r.End && r.TakeIf('('); } - private static bool PeekOpenBracket(Reader r) + private static bool PeekOpenBracket(CharacterReader r) { return !r.End && r.Peek == '['; } - private static bool ParseStreamOperator(Reader r) + private static bool ParseStreamOperator(CharacterReader r) { return !r.End && r.TakeIf('^'); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs index a05485f55b..cae6449722 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs @@ -2,6 +2,7 @@ using Avalonia.Data.Core; using Avalonia.Markup.Parsers; using Avalonia.Markup.Xaml.Parsers; +using Avalonia.Utilities; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Parsers @@ -12,7 +13,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Name() { var target = new PropertyParser(); - var reader = new Reader("Foo"); + var reader = new CharacterReader("Foo"); var (ns, owner, name) = target.Parse(reader); Assert.Null(ns); @@ -24,7 +25,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Owner_And_Name() { var target = new PropertyParser(); - var reader = new Reader("Foo.Bar"); + var reader = new CharacterReader("Foo.Bar"); var (ns, owner, name) = target.Parse(reader); Assert.Null(ns); @@ -36,7 +37,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Namespace_Owner_And_Name() { var target = new PropertyParser(); - var reader = new Reader("foo:Bar.Baz"); + var reader = new CharacterReader("foo:Bar.Baz"); var (ns, owner, name) = target.Parse(reader); Assert.Equal("foo", ns); @@ -48,7 +49,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Owner_And_Name_With_Parentheses() { var target = new PropertyParser(); - var reader = new Reader("(Foo.Bar)"); + var reader = new CharacterReader("(Foo.Bar)"); var (ns, owner, name) = target.Parse(reader); Assert.Null(ns); @@ -60,7 +61,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Namespace_Owner_And_Name_With_Parentheses() { var target = new PropertyParser(); - var reader = new Reader("(foo:Bar.Baz)"); + var reader = new CharacterReader("(foo:Bar.Baz)"); var (ns, owner, name) = target.Parse(reader); Assert.Equal("foo", ns); @@ -72,7 +73,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Empty_String() { var target = new PropertyParser(); - var reader = new Reader(""); + var reader = new CharacterReader(""); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(0, ex.Column); @@ -83,7 +84,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Only_Whitespace() { var target = new PropertyParser(); - var reader = new Reader(" "); + var reader = new CharacterReader(" "); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(0, ex.Column); @@ -94,7 +95,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Leading_Whitespace() { var target = new PropertyParser(); - var reader = new Reader(" Foo"); + var reader = new CharacterReader(" Foo"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(0, ex.Column); @@ -105,7 +106,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Trailing_Whitespace() { var target = new PropertyParser(); - var reader = new Reader("Foo "); + var reader = new CharacterReader("Foo "); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(3, ex.Column); @@ -116,7 +117,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Invalid_Property_Name() { var target = new PropertyParser(); - var reader = new Reader("123"); + var reader = new CharacterReader("123"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(0, ex.Column); @@ -127,7 +128,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Trailing_Junk() { var target = new PropertyParser(); - var reader = new Reader("Foo%"); + var reader = new CharacterReader("Foo%"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(3, ex.Column); @@ -138,7 +139,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Invalid_Property_Name_After_Owner() { var target = new PropertyParser(); - var reader = new Reader("Foo.123"); + var reader = new CharacterReader("Foo.123"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(4, ex.Column); @@ -149,7 +150,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Whitespace_Between_Owner_And_Name() { var target = new PropertyParser(); - var reader = new Reader("Foo. Bar"); + var reader = new CharacterReader("Foo. Bar"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(4, ex.Column); @@ -160,7 +161,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Too_Many_Segments() { var target = new PropertyParser(); - var reader = new Reader("Foo.Bar.Baz"); + var reader = new CharacterReader("Foo.Bar.Baz"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(8, ex.Column); @@ -171,7 +172,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Too_Many_Namespaces() { var target = new PropertyParser(); - var reader = new Reader("foo:bar:Baz"); + var reader = new CharacterReader("foo:bar:Baz"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(8, ex.Column); @@ -182,7 +183,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Parens_But_No_Owner() { var target = new PropertyParser(); - var reader = new Reader("(Foo)"); + var reader = new CharacterReader("(Foo)"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(1, ex.Column); @@ -193,7 +194,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Parens_And_Namespace_But_No_Owner() { var target = new PropertyParser(); - var reader = new Reader("(foo:Bar)"); + var reader = new CharacterReader("(foo:Bar)"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(1, ex.Column); @@ -204,7 +205,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Missing_Close_Parens() { var target = new PropertyParser(); - var reader = new Reader("(Foo.Bar"); + var reader = new CharacterReader("(Foo.Bar"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(8, ex.Column); @@ -215,7 +216,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Unexpected_Close_Parens() { var target = new PropertyParser(); - var reader = new Reader("Foo.Bar)"); + var reader = new CharacterReader("Foo.Bar)"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(7, ex.Column); From 728d5ae588c16531db285c6e717ce3bdb906baad Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 21 Jul 2018 11:09:56 +0800 Subject: [PATCH 178/211] Check if sender is equal to the argument. Cancel if it doesn't match. --- src/Avalonia.Animation/Animation.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 46b638dcf4..da4b3af38d 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -167,12 +167,18 @@ namespace Avalonia.Animation var tcs = new TaskCompletionSource(); if (this.RepeatCount == RepeatCount.Loop) + { tcs.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); + return tcs.Task; + } - this.Done += delegate + this.Done += (sender, args) => { - tcs.SetResult(null); - } + if (sender == control) + tcs.SetResult(null); + else + tcs.SetCanceled(); + }; this.Apply(control, Observable.Return(true)); From 25808ea5d9b777773e6135fafb08dfd05b1cbec6 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 21 Jul 2018 11:15:09 +0800 Subject: [PATCH 179/211] Change Run to RunAsync for convention's sake. --- src/Avalonia.Animation/Animation.cs | 15 ++++++--------- src/Avalonia.Animation/IAnimation.cs | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index da4b3af38d..21b25d2515 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -162,27 +162,24 @@ namespace Avalonia.Animation } /// - public Task Run(Animatable control) + public Task RunAsync(Animatable control) { - var tcs = new TaskCompletionSource(); + var run = new TaskCompletionSource(); if (this.RepeatCount == RepeatCount.Loop) - { - tcs.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); - return tcs.Task; - } + run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); this.Done += (sender, args) => { if (sender == control) - tcs.SetResult(null); + run.SetResult(null); else - tcs.SetCanceled(); + run.SetCanceled(); }; this.Apply(control, Observable.Return(true)); - return tcs.Task; + return run.Task; } internal void SetDone(Animatable control) diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index 7ceb6d5fe4..734eb3e479 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -18,6 +18,6 @@ namespace Avalonia.Animation /// /// Run the animation to the specified control /// - Task Run(Animatable control); + Task RunAsync(Animatable control); } } \ No newline at end of file From 6642eb49cb13a22bbadc06471f3a237cb25a2cae Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 21 Jul 2018 14:35:14 +0800 Subject: [PATCH 180/211] Fix wrongly placed SetCanceled call --- src/Avalonia.Animation/Animation.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 21b25d2515..9753888ea1 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -173,8 +173,6 @@ namespace Avalonia.Animation { if (sender == control) run.SetResult(null); - else - run.SetCanceled(); }; this.Apply(control, Observable.Return(true)); From 4ce176cd11c065e9f273880a7f6ff7eb843fb571 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 21 Jul 2018 17:12:00 -0500 Subject: [PATCH 181/211] Null check calling the Done callback. --- src/Avalonia.Animation/Animation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 9753888ea1..67b937e8ba 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -182,7 +182,7 @@ namespace Avalonia.Animation internal void SetDone(Animatable control) { - Done.Invoke(control, null); + Done?.Invoke(control, null); } } -} \ No newline at end of file +} From a098bf2d6f6f36f6e4ff742be73ed90ec531e75c Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 21 Jul 2018 17:46:21 -0500 Subject: [PATCH 182/211] Dispose state machine on animation completion. --- src/Avalonia.Animation/AnimatorStateMachine`1.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index b5138ad26b..4bffd5c145 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -246,7 +246,7 @@ namespace Avalonia.Animation _targetObserver.OnCompleted(); _targetAnimation.SetDone(_targetControl); - + Dispose(); handled = true; break; default: From 8464a480e003c38cf136f9a0ceffc5120e039bae Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 21 Jul 2018 17:49:50 -0500 Subject: [PATCH 183/211] Remove callback on animation completion. --- src/Avalonia.Animation/Animation.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 67b937e8ba..98fb652c4a 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -169,12 +169,18 @@ namespace Avalonia.Animation if (this.RepeatCount == RepeatCount.Loop) run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); - this.Done += (sender, args) => + EventHandler doneCallback = null; + doneCallback = (sender, args) => { if (sender == control) + { run.SetResult(null); + this.Done -= doneCallback; + } }; + this.Done += doneCallback; + this.Apply(control, Observable.Return(true)); return run.Task; From 1cda0ba30e26b6adcec9cffad3d54713c341050d Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 21 Jul 2018 17:50:38 -0500 Subject: [PATCH 184/211] Reimplement CrossFade. --- src/Avalonia.Visuals/Animation/CrossFade.cs | 69 ++++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 410ad0a3b3..307fb691ac 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Reactive.Threading.Tasks; using System.Threading.Tasks; +using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Animation @@ -14,10 +15,14 @@ namespace Avalonia.Animation /// public class CrossFade : IPageTransition { + private Animation _fadeOutAnimation; + private Animation _fadeInAnimation; + /// /// Initializes a new instance of the class. /// public CrossFade() + :this(TimeSpan.Zero) { } @@ -27,13 +32,62 @@ namespace Avalonia.Animation /// The duration of the animation. public CrossFade(TimeSpan duration) { - Duration = duration; + _fadeOutAnimation = new Animation + { + new KeyFrame + ( + new Setter + { + Property = Visual.OpacityProperty, + Value = 0.0 + } + ) + { + Cue = new Cue(1.0) + } + }; + _fadeInAnimation = new Animation + { + new KeyFrame + ( + new Setter + { + Property = Visual.OpacityProperty, + Value = 0.0 + } + ) + { + Cue = new Cue(0.0) + }, + new KeyFrame + ( + new Setter + { + Property = Visual.OpacityProperty, + Value = 1.0 + } + ) + { + Cue = new Cue(1.0) + } + }; + _fadeOutAnimation.Duration = _fadeInAnimation.Duration = duration; } /// /// Gets the duration of the animation. /// - public TimeSpan Duration { get; set; } + public TimeSpan Duration + { + get + { + return _fadeOutAnimation.Duration; + } + set + { + _fadeOutAnimation.Duration = _fadeInAnimation.Duration = value; + } + } /// /// Starts the animation. @@ -51,26 +105,25 @@ namespace Avalonia.Animation { var tasks = new List(); - // TODO: Implement relevant transition logic here (or discard this class) - // in favor of XAML based transition for pages if (to != null) { to.Opacity = 0; } - if (from != null) + if (from is Animatable fadeOut) { + tasks.Add(_fadeOutAnimation.RunAsync(fadeOut)); } - if (to != null) + if (to is Animatable fadeIn) { to.Opacity = 0; to.IsVisible = true; + tasks.Add(_fadeInAnimation.RunAsync(fadeIn)); } - // FIXME: This is temporary until animations are fixed. - await Task.Delay(1); + await Task.WhenAll(tasks); if (from != null) { From a87c90086a15a34922f89a9e1aac78b18129e573 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 21 Jul 2018 18:43:47 -0500 Subject: [PATCH 185/211] Re-implement PageSlide. --- src/Avalonia.Controls/Expander.cs | 6 +- src/Avalonia.Visuals/Animation/CrossFade.cs | 25 ++------ .../Animation/IPageTransition.cs | 2 +- src/Avalonia.Visuals/Animation/PageSlide.cs | 64 +++++++++++++++++-- 4 files changed, 68 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index e7f75336f5..5323939b50 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -66,9 +66,7 @@ namespace Avalonia.Controls protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e) { - IVisual visualContent = Presenter; - - if (Content != null && ContentTransition != null && visualContent != null) + if (Content != null && ContentTransition != null && Presenter is Visual visualContent) { bool forward = ExpandDirection == ExpandDirection.Left || ExpandDirection == ExpandDirection.Up; @@ -87,4 +85,4 @@ namespace Avalonia.Controls private ExpandDirection _expandDirection; private bool _isExpanded; } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 307fb691ac..2230f8534b 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -58,17 +58,6 @@ namespace Avalonia.Animation ) { Cue = new Cue(0.0) - }, - new KeyFrame - ( - new Setter - { - Property = Visual.OpacityProperty, - Value = 1.0 - } - ) - { - Cue = new Cue(1.0) } }; _fadeOutAnimation.Duration = _fadeInAnimation.Duration = duration; @@ -101,7 +90,7 @@ namespace Avalonia.Animation /// /// A that tracks the progress of the animation. /// - public async Task Start(IVisual from, IVisual to) + public async Task Start(Visual from, Visual to) { var tasks = new List(); @@ -110,16 +99,15 @@ namespace Avalonia.Animation to.Opacity = 0; } - if (from is Animatable fadeOut) + if (from != null) { - tasks.Add(_fadeOutAnimation.RunAsync(fadeOut)); + tasks.Add(_fadeOutAnimation.RunAsync(from)); } - if (to is Animatable fadeIn) + if (to != null) { - to.Opacity = 0; to.IsVisible = true; - tasks.Add(_fadeInAnimation.RunAsync(fadeIn)); + tasks.Add(_fadeInAnimation.RunAsync(to)); } @@ -128,7 +116,6 @@ namespace Avalonia.Animation if (from != null) { from.IsVisible = false; - from.Opacity = 1; } if (to != null) @@ -152,7 +139,7 @@ namespace Avalonia.Animation /// /// A that tracks the progress of the animation. /// - Task IPageTransition.Start(IVisual from, IVisual to, bool forward) + Task IPageTransition.Start(Visual from, Visual to, bool forward) { return Start(from, to); } diff --git a/src/Avalonia.Visuals/Animation/IPageTransition.cs b/src/Avalonia.Visuals/Animation/IPageTransition.cs index 6dc64de049..88912c1931 100644 --- a/src/Avalonia.Visuals/Animation/IPageTransition.cs +++ b/src/Avalonia.Visuals/Animation/IPageTransition.cs @@ -26,6 +26,6 @@ namespace Avalonia.Animation /// /// A that tracks the progress of the animation. /// - Task Start(IVisual from, IVisual to, bool forward); + Task Start(Visual from, Visual to, bool forward); } } diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Visuals/Animation/PageSlide.cs index 13f9e67d29..5f03dc6b0c 100644 --- a/src/Avalonia.Visuals/Animation/PageSlide.cs +++ b/src/Avalonia.Visuals/Animation/PageSlide.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Reactive.Threading.Tasks; using System.Threading.Tasks; using Avalonia.Media; +using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Animation @@ -67,7 +68,7 @@ namespace Avalonia.Animation /// /// A that tracks the progress of the animation. /// - public async Task Start(IVisual from, IVisual to, bool forward) + public async Task Start(Visual from, Visual to, bool forward) { var tasks = new List(); var parent = GetVisualParent(from, to); @@ -79,16 +80,69 @@ namespace Avalonia.Animation // in favor of XAML based transition for pages if (from != null) { - + var animation = new Animation + { + new KeyFrame + ( + new Setter + { + Property = translateProperty, + Value = 0 + } + ) + { + Cue = new Cue(0.0) + }, + new KeyFrame + ( + new Setter + { + Property = translateProperty, + Value = forward ? -distance : distance + } + ) + { + Cue = new Cue(1.0) + } + }; + animation.Duration = Duration; + tasks.Add(animation.RunAsync(from)); } if (to != null) { - + to.IsVisible = true; + var animation = new Animation + { + + new KeyFrame + ( + new Setter + { + Property = translateProperty, + Value = forward ? -distance : distance + } + ) + { + Cue = new Cue(0.0) + }, + new KeyFrame + ( + new Setter + { + Property = translateProperty, + Value = 0 + } + ) + { + Cue = new Cue(1.0) + }, + }; + animation.Duration = Duration; + tasks.Add(animation.RunAsync(to)); } - // FIXME: This is temporary until animations are fixed. - await Task.Delay(1); + await Task.WhenAll(tasks); if (from != null) { From 5b5971aff37497df9a19fec944126dc2632bedc2 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sun, 22 Jul 2018 19:15:12 +0200 Subject: [PATCH 186/211] Comment fixes --- .gitignore | 2 +- src/Avalonia.Visuals/Media/DrawingContext.cs | 2 +- src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs | 2 +- src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs | 2 +- src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs | 6 +++--- src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index dd814ba26f..583a2b8a2b 100644 --- a/.gitignore +++ b/.gitignore @@ -181,4 +181,4 @@ project.lock.json ################## ## BenchmarkDotNet ################## -BenchmarkDotNet.Artifacts/ \ No newline at end of file +BenchmarkDotNet.Artifacts/ diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 173801720d..60a7a2e518 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -69,7 +69,7 @@ namespace Avalonia.Media /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - /// + /// The bitmap interpolation mode. public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) { Contract.Requires(source != null); diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs index c7a6a168d7..c3dd159d04 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs @@ -22,7 +22,7 @@ namespace Avalonia.Media.Immutable /// How the source rectangle will be stretched to fill the destination rect. /// /// The tile mode. - /// Controls the quality of interpolation. + /// The bitmap interpolation mode. protected ImmutableTileBrush( AlignmentX alignmentX, AlignmentY alignmentY, diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index 4ddbbf4dd5..3ffcde7165 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -32,7 +32,7 @@ namespace Avalonia.Platform /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - /// Controls + /// The bitmap interpolation mode. void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default); /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index 35fad25276..9e8fca5f84 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -21,7 +21,7 @@ namespace Avalonia.Rendering.SceneGraph /// The draw opacity. /// The source rect. /// The destination rect. - /// The bitmap scaling mode. + /// The bitmap interpolation mode. public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) : base(destRect, transform, null) { @@ -59,7 +59,7 @@ namespace Avalonia.Rendering.SceneGraph public Rect DestRect { get; } /// - /// Gets the bitmap scaling mode. + /// Gets the bitmap interpolation mode. /// /// /// The scaling mode. @@ -74,7 +74,7 @@ namespace Avalonia.Rendering.SceneGraph /// The opacity of the other draw operation. /// The source rect of the other draw operation. /// The dest rect of the other draw operation. - /// The bitmap scaling mode. + /// The bitmap interpolation mode. /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index bf379bac5c..659c89bb58 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -103,7 +103,7 @@ namespace Avalonia.Direct2D1.Media /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - /// + /// The bitmap interpolation mode. public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { using (var d2d = ((BitmapImpl)source.Item).GetDirect2DBitmap(_renderTarget)) From 81008bd5f7189c07b5edb6b2d7de3881f73f15c7 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sun, 22 Jul 2018 19:42:24 +0200 Subject: [PATCH 187/211] Naming fixes --- src/Avalonia.Controls/Image.cs | 2 +- src/Avalonia.Visuals/Media/RenderOptions.cs | 8 ++++---- src/Avalonia.Visuals/Media/TileBrush.cs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index ffccfdf081..50e4f1515e 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -68,7 +68,7 @@ namespace Avalonia.Controls Rect sourceRect = new Rect(sourceSize) .CenterRect(new Rect(destRect.Size / scale)); - var scalingMode = RenderOptions.GetBitmapScalingMode(this); + var scalingMode = RenderOptions.GetBitmapInterpolationMode(this); context.DrawImage(source, 1, sourceRect, destRect, scalingMode); } diff --git a/src/Avalonia.Visuals/Media/RenderOptions.cs b/src/Avalonia.Visuals/Media/RenderOptions.cs index 7efe1c7349..b38ba3f9c9 100644 --- a/src/Avalonia.Visuals/Media/RenderOptions.cs +++ b/src/Avalonia.Visuals/Media/RenderOptions.cs @@ -3,12 +3,12 @@ using Avalonia.Visuals.Media.Imaging; -namespace Avalonia.Visuals.Media +namespace Avalonia.Media { public class RenderOptions { /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty BitmapInterpolationMode = AvaloniaProperty.RegisterAttached( @@ -20,7 +20,7 @@ namespace Avalonia.Visuals.Media /// /// The control. /// The control's left coordinate. - public static BitmapInterpolationMode GetBitmapScalingMode(AvaloniaObject element) + public static BitmapInterpolationMode GetBitmapInterpolationMode(AvaloniaObject element) { return element.GetValue(BitmapInterpolationMode); } @@ -30,7 +30,7 @@ namespace Avalonia.Visuals.Media /// /// The control. /// The left value. - public static void SetBitmapScalingMode(AvaloniaObject element, BitmapInterpolationMode value) + public static void SetBitmapInterpolationMode(AvaloniaObject element, BitmapInterpolationMode value) { element.SetValue(BitmapInterpolationMode, value); } diff --git a/src/Avalonia.Visuals/Media/TileBrush.cs b/src/Avalonia.Visuals/Media/TileBrush.cs index 5a6da8f2e1..506b406856 100644 --- a/src/Avalonia.Visuals/Media/TileBrush.cs +++ b/src/Avalonia.Visuals/Media/TileBrush.cs @@ -141,8 +141,8 @@ namespace Avalonia.Media /// public BitmapInterpolationMode BitmapInterpolationMode { - get { return RenderOptions.GetBitmapScalingMode(this); } - set { RenderOptions.SetBitmapScalingMode(this, value); } + get { return RenderOptions.GetBitmapInterpolationMode(this); } + set { RenderOptions.SetBitmapInterpolationMode(this, value); } } } } From 4c63eb37d2459886b9fbfaba19577e38aa448d7f Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 23 Jul 2018 09:17:06 +0200 Subject: [PATCH 188/211] Namespace fixes --- src/Avalonia.Controls/Image.cs | 1 - src/Avalonia.Controls/Remote/RemoteWidget.cs | 4 ++-- src/Avalonia.Visuals/Media/ITileBrush.cs | 9 ++++++--- src/Avalonia.Visuals/Media/TileBrush.cs | 5 ++--- src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs | 3 +-- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 -- .../Avalonia.Direct2D1/Media/DrawingContextImpl.cs | 6 ++---- .../Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs | 10 +++------- 8 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 50e4f1515e..fd90f9f2e8 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -3,7 +3,6 @@ using Avalonia.Media; using Avalonia.Media.Imaging; -using Avalonia.Visuals.Media; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index 618a0c3f62..ea8c3ebe52 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -9,7 +9,7 @@ using Avalonia.Threading; using PixelFormat = Avalonia.Platform.PixelFormat; namespace Avalonia.Controls.Remote -{ +{ public class RemoteWidget : Control { private readonly IAvaloniaRemoteTransportConnection _connection; @@ -76,4 +76,4 @@ namespace Avalonia.Controls.Remote base.Render(context); } } -} +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/ITileBrush.cs b/src/Avalonia.Visuals/Media/ITileBrush.cs index 6c29b159de..5cffe02193 100644 --- a/src/Avalonia.Visuals/Media/ITileBrush.cs +++ b/src/Avalonia.Visuals/Media/ITileBrush.cs @@ -1,7 +1,10 @@ -namespace Avalonia.Media -{ - using Avalonia.Visuals.Media.Imaging; +// 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 Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Media +{ /// /// A brush which displays a repeating image. /// diff --git a/src/Avalonia.Visuals/Media/TileBrush.cs b/src/Avalonia.Visuals/Media/TileBrush.cs index 506b406856..094208d021 100644 --- a/src/Avalonia.Visuals/Media/TileBrush.cs +++ b/src/Avalonia.Visuals/Media/TileBrush.cs @@ -1,11 +1,10 @@ // 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 Avalonia.Visuals.Media.Imaging; + namespace Avalonia.Media { - using Avalonia.Visuals.Media; - using Avalonia.Visuals.Media.Imaging; - /// /// Describes how a is tiled. /// diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index 3ffcde7165..debc7103c0 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -4,11 +4,10 @@ using System; using Avalonia.Media; using Avalonia.Utilities; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Platform { - using Avalonia.Visuals.Media.Imaging; - /// /// Defines the interface through which drawing occurs. /// diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 7903e6cf6e..28ac311605 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -15,8 +15,6 @@ using SkiaSharp; namespace Avalonia.Skia { - using Avalonia.Controls; - /// /// Skia based drawing context. /// diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 659c89bb58..ae5dd3ae13 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -7,19 +7,17 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Utilities; -using Avalonia.Visuals.Media.Imaging; using SharpDX; using SharpDX.Direct2D1; using SharpDX.Mathematics.Interop; +using BitmapInterpolationMode = Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode; namespace Avalonia.Direct2D1.Media { - using BitmapInterpolationMode = Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode; - /// /// Draws using Direct2D1. /// - public class DrawingContextImpl : IDrawingContextImpl, IDisposable + public class DrawingContextImpl : IDrawingContextImpl { private readonly IVisualBrushRenderer _visualBrushRenderer; private readonly ILayerFactory _layerFactory; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs index c792334ec1..d58f023391 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs @@ -1,15 +1,11 @@ -// 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; +using System; using System.IO; using Avalonia.Platform; using SharpDX.WIC; +using D2DBitmap = SharpDX.Direct2D1.Bitmap; namespace Avalonia.Direct2D1.Media { - using SharpDX.Direct2D1; - public abstract class BitmapImpl : IBitmapImpl, IDisposable { public BitmapImpl(ImagingFactory imagingFactory) @@ -21,7 +17,7 @@ namespace Avalonia.Direct2D1.Media public abstract int PixelWidth { get; } public abstract int PixelHeight { get; } - public abstract OptionalDispose GetDirect2DBitmap(RenderTarget target); + public abstract OptionalDispose GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget target); public void Save(string fileName) { From f4b0e22963eb51a6dd4b0ea111a1bbf64d374fd0 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 23 Jul 2018 19:06:22 +0200 Subject: [PATCH 189/211] Set default to high quality --- src/Avalonia.Visuals/Media/RenderOptions.cs | 11 ++++++----- src/Avalonia.Visuals/Media/TileBrush.cs | 5 +++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Visuals/Media/RenderOptions.cs b/src/Avalonia.Visuals/Media/RenderOptions.cs index b38ba3f9c9..5e16f5f9ce 100644 --- a/src/Avalonia.Visuals/Media/RenderOptions.cs +++ b/src/Avalonia.Visuals/Media/RenderOptions.cs @@ -8,11 +8,12 @@ namespace Avalonia.Media public class RenderOptions { /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty BitmapInterpolationMode = + public static readonly StyledProperty BitmapInterpolationModeProperty = AvaloniaProperty.RegisterAttached( - "BitmapInterpolationMode", + "BitmapInterpolationMode", + BitmapInterpolationMode.HighQuality, inherits: true); /// @@ -22,7 +23,7 @@ namespace Avalonia.Media /// The control's left coordinate. public static BitmapInterpolationMode GetBitmapInterpolationMode(AvaloniaObject element) { - return element.GetValue(BitmapInterpolationMode); + return element.GetValue(BitmapInterpolationModeProperty); } /// @@ -32,7 +33,7 @@ namespace Avalonia.Media /// The left value. public static void SetBitmapInterpolationMode(AvaloniaObject element, BitmapInterpolationMode value) { - element.SetValue(BitmapInterpolationMode, value); + element.SetValue(BitmapInterpolationModeProperty, value); } } } diff --git a/src/Avalonia.Visuals/Media/TileBrush.cs b/src/Avalonia.Visuals/Media/TileBrush.cs index 094208d021..2033754137 100644 --- a/src/Avalonia.Visuals/Media/TileBrush.cs +++ b/src/Avalonia.Visuals/Media/TileBrush.cs @@ -77,6 +77,11 @@ namespace Avalonia.Media public static readonly StyledProperty TileModeProperty = AvaloniaProperty.Register(nameof(TileMode)); + static TileBrush() + { + RenderOptions.BitmapInterpolationModeProperty.OverrideDefaultValue(BitmapInterpolationMode.Default); + } + /// /// Gets or sets the horizontal alignment of a tile in the destination. /// From 70f3ba800ef49951750e14d9931c9a20e62c2810 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Mon, 23 Jul 2018 15:00:54 -0400 Subject: [PATCH 190/211] Added more linux keys & added VS code config to .gitignore --- .gitignore | 5 +++++ src/Gtk/Avalonia.Gtk3/KeyTransform.cs | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/.gitignore b/.gitignore index 583a2b8a2b..32acee4c90 100644 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,11 @@ $RECYCLE.BIN/ ################# .idea +################# +## VS Code +################# +.vscode/ + ################# ## Cake ################# diff --git a/src/Gtk/Avalonia.Gtk3/KeyTransform.cs b/src/Gtk/Avalonia.Gtk3/KeyTransform.cs index 5a34db2e04..69c5a1bf5f 100644 --- a/src/Gtk/Avalonia.Gtk3/KeyTransform.cs +++ b/src/Gtk/Avalonia.Gtk3/KeyTransform.cs @@ -33,17 +33,24 @@ namespace Avalonia.Gtk.Common { GdkKey.Prior, Key.Prior }, //{ GdkKey.?, Key.PageDown } { GdkKey.End, Key.End }, + { GdkKey.KP_End, Key.End }, { GdkKey.Home, Key.Home }, + { GdkKey.KP_Home, Key.Home }, { GdkKey.Left, Key.Left }, + { GdkKey.KP_Left, Key.Left }, { GdkKey.Up, Key.Up }, + { GdkKey.KP_Up, Key.Up }, { GdkKey.Right, Key.Right }, + { GdkKey.KP_Right, Key.Right }, { GdkKey.Down, Key.Down }, + { GdkKey.KP_Down, Key.Down }, { GdkKey.Select, Key.Select }, { GdkKey.Print, Key.Print }, { GdkKey.Execute, Key.Execute }, //{ GdkKey.?, Key.Snapshot } { GdkKey.Insert, Key.Insert }, { GdkKey.Delete, Key.Delete }, + { GdkKey.KP_Delete, Key.Delete }, { GdkKey.Help, Key.Help }, //{ GdkKey.?, Key.D0 } //{ GdkKey.?, Key.D1 } From cdf8d1f8b4810c86a2eaa56ade3a66dedd8efff5 Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Mon, 23 Jul 2018 21:09:40 +0200 Subject: [PATCH 191/211] Fix For incorrect handling of NotifyCollectionChangedAction.Reset from IReactiveDerivedList<> --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index eb3fbde8f2..c8425a0f80 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -376,7 +376,7 @@ namespace Avalonia.Controls.Primitives break; case NotifyCollectionChangedAction.Reset: - SelectedIndex = IndexOf(e.NewItems, SelectedItem); + SelectedIndex = IndexOf(Items, SelectedItem); break; } } From 03e859445113e8f2b3ed34383a893cb19ecf517e Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Mon, 23 Jul 2018 22:32:13 +0200 Subject: [PATCH 192/211] Bug reproduction unit test --- .../Primitives/SelectingItemsControlTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index c7a3465ac4..14e1b15ebc 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; @@ -13,6 +14,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Data; using Avalonia.UnitTests; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives @@ -686,6 +688,26 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Null(KeyboardNavigation.GetTabOnceActiveElement((InputElement)panel)); } + [Fact] + public void Resetting_Items_Collection_Should_Retain_Selection() + { + var itemsMock = new Mock>(); + var itemsMockAsINCC = itemsMock.As(); + + itemsMock.Object.AddRange(new[] { "Foo", "Bar", "Baz" }); + var target = new SelectingItemsControl + { + Items = itemsMock.Object + }; + + target.SelectedIndex = 1; + + itemsMockAsINCC.Raise(e => e.CollectionChanged += null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + + Assert.True(target.SelectedIndex == 1); + } + + private FuncControlTemplate Template() { return new FuncControlTemplate(control => From 7e59520aaff11a8ab24817d446f65fc23a94f2c4 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 25 Jul 2018 13:44:01 +0200 Subject: [PATCH 193/211] Set default render quality to medium --- src/Avalonia.Visuals/Media/RenderOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/RenderOptions.cs b/src/Avalonia.Visuals/Media/RenderOptions.cs index 5e16f5f9ce..180863f9e8 100644 --- a/src/Avalonia.Visuals/Media/RenderOptions.cs +++ b/src/Avalonia.Visuals/Media/RenderOptions.cs @@ -13,7 +13,7 @@ namespace Avalonia.Media public static readonly StyledProperty BitmapInterpolationModeProperty = AvaloniaProperty.RegisterAttached( "BitmapInterpolationMode", - BitmapInterpolationMode.HighQuality, + BitmapInterpolationMode.MediumQuality, inherits: true); /// From 23fdfe51df8ddb4f8d46203db7be7207edb431d2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 25 Jul 2018 21:35:44 +0100 Subject: [PATCH 194/211] fix bug that was preventing user from cancelling window close. --- src/Avalonia.Controls/Window.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index d1a023c42c..7e1d8f18f0 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -302,17 +302,23 @@ namespace Avalonia.Controls internal void Close(bool ignoreCancel) { + bool close = true; + try { if (!ignoreCancel && HandleClosing()) { + close = false; return; } } finally { - PlatformImpl?.Dispose(); - HandleClosed(); + if (close) + { + PlatformImpl?.Dispose(); + HandleClosed(); + } } } From fb30bcd9bd1cd24e045e688e22c09036bdad5bd3 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Thu, 26 Jul 2018 08:58:54 +0200 Subject: [PATCH 195/211] Some minor issues fixed --- src/Avalonia.Controls/Image.cs | 4 ++-- src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs | 2 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index fd90f9f2e8..f146e3571c 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -67,9 +67,9 @@ namespace Avalonia.Controls Rect sourceRect = new Rect(sourceSize) .CenterRect(new Rect(destRect.Size / scale)); - var scalingMode = RenderOptions.GetBitmapInterpolationMode(this); + var interpolationMode = RenderOptions.GetBitmapInterpolationMode(this); - context.DrawImage(source, 1, sourceRect, destRect, scalingMode); + context.DrawImage(source, 1, sourceRect, destRect, interpolationMode); } } diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index debc7103c0..b11d9f52ab 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -32,7 +32,7 @@ namespace Avalonia.Platform /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default); + void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); /// /// Draws a bitmap image. diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 28ac311605..685987ae80 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -381,7 +381,7 @@ namespace Avalonia.Skia /// Tile brush to use. /// Tile brush image. /// The bitmap interpolation mode. - private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage, BitmapInterpolationMode interpolationMode) + private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage) { var calc = new TileBrushCalculator(tileBrush, new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize); @@ -399,7 +399,7 @@ namespace Avalonia.Skia context.Clear(Colors.Transparent); context.PushClip(calc.IntermediateClip); context.Transform = calc.IntermediateTransform; - context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect, interpolationMode); + context.DrawImage(RefCountable.CreateUnownedNotClonable(tileBrushImage), 1, rect, rect, tileBrush.BitmapInterpolationMode); context.PopClip(); } @@ -513,7 +513,7 @@ namespace Avalonia.Skia if (tileBrush != null && tileBrushImage != null) { - ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage, tileBrush.BitmapInterpolationMode); + ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage); } else { From 5062f823b3135a82c530b01b83548135ed1e6450 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 26 Jul 2018 16:01:18 +0100 Subject: [PATCH 196/211] rename stackpanel Gap property to Spacing property. --- samples/BindingDemo/MainWindow.xaml | 26 ++++++------- .../Pages/AutoCompleteBoxPage.xaml | 6 +-- samples/ControlCatalog/Pages/BorderPage.xaml | 6 +-- samples/ControlCatalog/Pages/ButtonPage.xaml | 10 ++--- .../Pages/ButtonSpinnerPage.xaml | 6 +-- .../ControlCatalog/Pages/CalendarPage.xaml | 6 +-- samples/ControlCatalog/Pages/CanvasPage.xaml | 4 +- .../ControlCatalog/Pages/CarouselPage.xaml | 10 ++--- .../ControlCatalog/Pages/CheckBoxPage.xaml | 10 ++--- .../ControlCatalog/Pages/ContextMenuPage.xaml | 6 +-- .../ControlCatalog/Pages/DatePickerPage.xaml | 6 +-- samples/ControlCatalog/Pages/DialogsPage.xaml | 4 +- .../ControlCatalog/Pages/DragAndDropPage.xaml | 6 +-- .../ControlCatalog/Pages/DropDownPage.xaml | 6 +-- .../ControlCatalog/Pages/ExpanderPage.xaml | 6 +-- samples/ControlCatalog/Pages/ImagePage.xaml | 4 +- samples/ControlCatalog/Pages/MenuPage.xaml | 4 +- .../Pages/NumericUpDownPage.xaml | 6 +-- .../ControlCatalog/Pages/ProgressBarPage.xaml | 6 +-- .../ControlCatalog/Pages/RadioButtonPage.xaml | 8 ++-- samples/ControlCatalog/Pages/SliderPage.xaml | 4 +- samples/ControlCatalog/Pages/TextBoxPage.xaml | 10 ++--- samples/ControlCatalog/Pages/ToolTipPage.xaml | 2 +- .../ControlCatalog/Pages/TreeViewPage.xaml | 4 +- samples/VirtualizationDemo/MainWindow.xaml | 2 +- src/Avalonia.Controls/StackPanel.cs | 38 +++++++++---------- .../VirtualizingStackPanel.cs | 12 +++--- src/Avalonia.Diagnostics/DevTools.xaml | 4 +- .../Views/TreePageView.xaml | 4 +- .../StackPanelTests.cs | 14 +++---- 30 files changed, 120 insertions(+), 120 deletions(-) diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index a69fb75742..95713dc22f 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -18,18 +18,18 @@ - + - + - + !BooleanString @@ -37,13 +37,13 @@ - + - + @@ -52,7 +52,7 @@ - + @@ -68,11 +68,11 @@ - + - + @@ -87,16 +87,16 @@ - + - + - + @@ -104,7 +104,7 @@ - + @@ -25,7 +25,7 @@ - + @@ -33,4 +33,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml index 1797fb48bc..fba15f6e77 100644 --- a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml @@ -1,11 +1,11 @@  - + ButtonSpinner The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element. - + AllowSpin ShowButtonSpinner - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/CalendarPage.xaml b/samples/ControlCatalog/Pages/CalendarPage.xaml index a433fd1add..c47fd766fb 100644 --- a/samples/ControlCatalog/Pages/CalendarPage.xaml +++ b/samples/ControlCatalog/Pages/CalendarPage.xaml @@ -1,13 +1,13 @@ - + Calendar A calendar control for selecting dates + Spacing="16"> - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/CanvasPage.xaml b/samples/ControlCatalog/Pages/CanvasPage.xaml index f934f57c22..10a38895a2 100644 --- a/samples/ControlCatalog/Pages/CanvasPage.xaml +++ b/samples/ControlCatalog/Pages/CanvasPage.xaml @@ -1,5 +1,5 @@ - + Canvas A panel which lays out its children by explicit coordinates @@ -31,4 +31,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml b/samples/ControlCatalog/Pages/CarouselPage.xaml index 3468b71fd8..cf9b13c00c 100644 --- a/samples/ControlCatalog/Pages/CarouselPage.xaml +++ b/samples/ControlCatalog/Pages/CarouselPage.xaml @@ -1,9 +1,9 @@ - + Carousel An items control that displays its items as pages that fill the control. - + @@ -20,7 +20,7 @@ - + Transition None @@ -29,7 +29,7 @@ - + Orientation Horizontal @@ -38,4 +38,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/CheckBoxPage.xaml b/samples/ControlCatalog/Pages/CheckBoxPage.xaml index a00b3a7bef..154a6254a4 100644 --- a/samples/ControlCatalog/Pages/CheckBoxPage.xaml +++ b/samples/ControlCatalog/Pages/CheckBoxPage.xaml @@ -1,15 +1,15 @@ - + CheckBox A check box control + Spacing="16"> + Spacing="16"> Unchecked Checked Indeterminate @@ -17,7 +17,7 @@ + Spacing="16"> Three State: Unchecked Three State: Checked Three State: Indeterminate @@ -25,4 +25,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml b/samples/ControlCatalog/Pages/ContextMenuPage.xaml index 3af823befc..37eeaeb2ac 100644 --- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml +++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml @@ -1,12 +1,12 @@ - + Context Menu A right click menu that can be applied to any control. + Spacing="16"> @@ -33,4 +33,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/DatePickerPage.xaml b/samples/ControlCatalog/Pages/DatePickerPage.xaml index 92cfa7e178..2c34460fce 100644 --- a/samples/ControlCatalog/Pages/DatePickerPage.xaml +++ b/samples/ControlCatalog/Pages/DatePickerPage.xaml @@ -1,13 +1,13 @@ - + DatePicker A control for selecting dates with a calendar drop-down + Spacing="16"> @@ -43,4 +43,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml index c3e9435630..710d791f3a 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml @@ -1,5 +1,5 @@ - + @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index af679d2f9a..1f3cd3ff71 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -1,12 +1,12 @@ - + Drag+Drop Example of Drag+Drop capabilities + Spacing="16"> Drag Me @@ -16,4 +16,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/DropDownPage.xaml b/samples/ControlCatalog/Pages/DropDownPage.xaml index 0a7a88e331..5e2a3102e7 100644 --- a/samples/ControlCatalog/Pages/DropDownPage.xaml +++ b/samples/ControlCatalog/Pages/DropDownPage.xaml @@ -1,9 +1,9 @@ - + DropDown A drop-down list. - + Inline Items Inline Item 2 @@ -28,4 +28,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ExpanderPage.xaml b/samples/ControlCatalog/Pages/ExpanderPage.xaml index e32fa1caf1..91440929f5 100644 --- a/samples/ControlCatalog/Pages/ExpanderPage.xaml +++ b/samples/ControlCatalog/Pages/ExpanderPage.xaml @@ -1,12 +1,12 @@ - + Expander Expands to show nested content + Spacing="16"> Expanded content @@ -29,4 +29,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index dc93808f27..78fbf90192 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -1,12 +1,12 @@ - + Image Displays an image + Spacing="16"> No Stretch - + Menu A window menu + Spacing="16"> diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml index a5c911f47d..305bcd177c 100644 --- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml @@ -1,6 +1,6 @@  - + Numeric up-down control Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel. @@ -26,7 +26,7 @@ VerticalAlignment="Center" Margin="2"> - + @@ -69,7 +69,7 @@ - + Usage of NumericUpDown: - + ProgressBar A progress bar control @@ -7,8 +7,8 @@ - + Spacing="16"> + diff --git a/samples/ControlCatalog/Pages/RadioButtonPage.xaml b/samples/ControlCatalog/Pages/RadioButtonPage.xaml index d382b94f2c..0882817a9a 100644 --- a/samples/ControlCatalog/Pages/RadioButtonPage.xaml +++ b/samples/ControlCatalog/Pages/RadioButtonPage.xaml @@ -1,22 +1,22 @@ - + RadioButton Allows the selection of a single option of many + Spacing="16"> + Spacing="16"> Option 1 Option 2 Option 3 Disabled + Spacing="16"> Three States: Option 1 Three States: Option 2 Three States: Option 3 diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index e43968cb8e..6db71b5fcc 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -1,9 +1,9 @@ - + Slider A control that lets the user select from a range of values by moving a Thumb control along a Track. - + - + TextBox A control into which the user can input text - + Spacing="16"> + @@ -26,13 +26,13 @@ - + - + diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml index aa7d60bd11..ad832b9b82 100644 --- a/samples/ControlCatalog/Pages/ToolTipPage.xaml +++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml @@ -1,6 +1,6 @@ + Spacing="4"> ToolTip A control which pops up a hint when a control is hovered diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index 5806e58c27..1ab49dbb30 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -1,12 +1,12 @@ - + TreeView Displays a hierachical tree of data. + Spacing="16"> diff --git a/samples/VirtualizationDemo/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml index eb94253d27..730b61ed54 100644 --- a/samples/VirtualizationDemo/MainWindow.xaml +++ b/samples/VirtualizationDemo/MainWindow.xaml @@ -6,7 +6,7 @@ + Spacing="4"> - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty GapProperty = - AvaloniaProperty.Register(nameof(Gap)); + public static readonly StyledProperty SpacingProperty = + AvaloniaProperty.Register(nameof(Spacing)); /// /// Defines the property. @@ -29,17 +29,17 @@ namespace Avalonia.Controls /// static StackPanel() { - AffectsMeasure(GapProperty); + AffectsMeasure(SpacingProperty); AffectsMeasure(OrientationProperty); } /// - /// Gets or sets the size of the gap to place between child controls. + /// Gets or sets the size of the spacing to place between child controls. /// - public double Gap + public double Spacing { - get { return GetValue(GapProperty); } - set { SetValue(GapProperty, value); } + get { return GetValue(SpacingProperty); } + set { SetValue(SpacingProperty, value); } } /// @@ -152,7 +152,7 @@ namespace Avalonia.Controls double measuredWidth = 0; double measuredHeight = 0; - double gap = Gap; + double spacing = Spacing; bool hasVisibleChild = Children.Any(c => c.IsVisible); foreach (Control child in Children) @@ -162,23 +162,23 @@ namespace Avalonia.Controls if (Orientation == Orientation.Vertical) { - measuredHeight += size.Height + (child.IsVisible ? gap : 0); + measuredHeight += size.Height + (child.IsVisible ? spacing : 0); measuredWidth = Math.Max(measuredWidth, size.Width); } else { - measuredWidth += size.Width + (child.IsVisible ? gap : 0); + measuredWidth += size.Width + (child.IsVisible ? spacing : 0); measuredHeight = Math.Max(measuredHeight, size.Height); } } if (Orientation == Orientation.Vertical) { - measuredHeight -= (hasVisibleChild ? gap : 0); + measuredHeight -= (hasVisibleChild ? spacing : 0); } else { - measuredWidth -= (hasVisibleChild ? gap : 0); + measuredWidth -= (hasVisibleChild ? spacing : 0); } return new Size(measuredWidth, measuredHeight); @@ -194,7 +194,7 @@ namespace Avalonia.Controls var orientation = Orientation; double arrangedWidth = finalSize.Width; double arrangedHeight = finalSize.Height; - double gap = Gap; + double spacing = Spacing; bool hasVisibleChild = Children.Any(c => c.IsVisible); if (Orientation == Orientation.Vertical) @@ -217,25 +217,25 @@ namespace Avalonia.Controls Rect childFinal = new Rect(0, arrangedHeight, width, childHeight); ArrangeChild(child, childFinal, finalSize, orientation); arrangedWidth = Math.Max(arrangedWidth, childWidth); - arrangedHeight += childHeight + (child.IsVisible ? gap : 0); + arrangedHeight += childHeight + (child.IsVisible ? spacing : 0); } else { double height = Math.Max(childHeight, arrangedHeight); Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height); ArrangeChild(child, childFinal, finalSize, orientation); - arrangedWidth += childWidth + (child.IsVisible ? gap : 0); + arrangedWidth += childWidth + (child.IsVisible ? spacing : 0); arrangedHeight = Math.Max(arrangedHeight, childHeight); } } if (orientation == Orientation.Vertical) { - arrangedHeight = Math.Max(arrangedHeight - (hasVisibleChild ? gap : 0), finalSize.Height); + arrangedHeight = Math.Max(arrangedHeight - (hasVisibleChild ? spacing : 0), finalSize.Height); } else { - arrangedWidth = Math.Max(arrangedWidth - (hasVisibleChild ? gap : 0), finalSize.Width); + arrangedWidth = Math.Max(arrangedWidth - (hasVisibleChild ? spacing : 0), finalSize.Width); } return new Size(arrangedWidth, arrangedHeight); @@ -250,4 +250,4 @@ namespace Avalonia.Controls child.Arrange(rect); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index dee537029c..5f7b63c57a 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -200,7 +200,7 @@ namespace Avalonia.Controls private void UpdateAdd(IControl child) { var bounds = Bounds; - var gap = Gap; + var spacing = Spacing; child.Measure(_availableSpace); ++_averageCount; @@ -208,13 +208,13 @@ namespace Avalonia.Controls if (Orientation == Orientation.Vertical) { var height = child.DesiredSize.Height; - _takenSpace += height + gap; + _takenSpace += height + spacing; AddToAverageItemSize(height); } else { var width = child.DesiredSize.Width; - _takenSpace += width + gap; + _takenSpace += width + spacing; AddToAverageItemSize(width); } } @@ -222,18 +222,18 @@ namespace Avalonia.Controls private void UpdateRemove(IControl child) { var bounds = Bounds; - var gap = Gap; + var spacing = Spacing; if (Orientation == Orientation.Vertical) { var height = child.DesiredSize.Height; - _takenSpace -= height + gap; + _takenSpace -= height + spacing; RemoveFromAverageItemSize(height); } else { var width = child.DesiredSize.Width; - _takenSpace -= width + gap; + _takenSpace -= width + spacing; RemoveFromAverageItemSize(width); } diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml index bb1b2e841e..844670e794 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml +++ b/src/Avalonia.Diagnostics/DevTools.xaml @@ -7,7 +7,7 @@ - + Hold Ctrl+Shift over a control to inspect. Focused: @@ -17,4 +17,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Views/TreePageView.xaml index a715ca6fc5..57398851ad 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Views/TreePageView.xaml @@ -5,7 +5,7 @@ - + @@ -21,4 +21,4 @@ - \ No newline at end of file + diff --git a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs index dca2c5df35..ba2716775a 100644 --- a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs @@ -54,11 +54,11 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Lays_Out_Children_Vertically_With_Gap() + public void Lays_Out_Children_Vertically_With_Spacing() { var target = new StackPanel { - Gap = 10, + Spacing = 10, Children = { new Border { Height = 20, Width = 120 }, @@ -77,11 +77,11 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Lays_Out_Children_Horizontally_With_Gap() + public void Lays_Out_Children_Horizontally_With_Spacing() { var target = new StackPanel { - Gap = 10, + Spacing = 10, Orientation = Orientation.Horizontal, Children = { @@ -150,11 +150,11 @@ namespace Avalonia.Controls.UnitTests [Theory] [InlineData(Orientation.Horizontal)] [InlineData(Orientation.Vertical)] - public void Gap_Not_Added_For_Invisible_Children(Orientation orientation) + public void Spacing_Not_Added_For_Invisible_Children(Orientation orientation) { var targetThreeChildrenOneInvisble = new StackPanel { - Gap = 40, + Spacing = 40, Orientation = orientation, Children = { @@ -165,7 +165,7 @@ namespace Avalonia.Controls.UnitTests }; var targetTwoChildrenNoneInvisible = new StackPanel { - Gap = 40, + Spacing = 40, Orientation = orientation, Children = { From ca7204ad9a13f8e415ab0d075c8dc61a4ea58279 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 26 Jul 2018 16:55:53 +0100 Subject: [PATCH 197/211] freeze designer support assembly version. --- .../Avalonia.DesignerSupport.csproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index 9f54137e47..5ccb98b64d 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -1,7 +1,12 @@  netstandard2.0 - false + + 0.7.0 true @@ -37,11 +42,6 @@ - - - Properties\SharedAssemblyInfo.cs - - \ No newline at end of file From 14ed0191aff36768e57173a16b3d83307d413704 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 26 Jul 2018 17:41:58 +0100 Subject: [PATCH 198/211] Dont freeze if someone implements window close cancellation. --- src/Avalonia.Controls/WindowCollection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/WindowCollection.cs b/src/Avalonia.Controls/WindowCollection.cs index c21a12f05b..df79c3e3c8 100644 --- a/src/Avalonia.Controls/WindowCollection.cs +++ b/src/Avalonia.Controls/WindowCollection.cs @@ -96,7 +96,7 @@ namespace Avalonia { while (_windows.Count > 0) { - _windows[0].Close(); + _windows[0].Close(true); } } @@ -131,4 +131,4 @@ namespace Avalonia } } } -} \ No newline at end of file +} From 079302a1233d64ee4f738ea6aac464ffa1ef7a7b Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 27 Jul 2018 16:39:35 -0500 Subject: [PATCH 199/211] Fix bug in AnimatorKeyFrame.ValueProperty registration. --- src/Avalonia.Animation/AnimatorKeyFrame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index bd9c7a0184..0276c6fa92 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -16,7 +16,7 @@ namespace Avalonia.Animation public class AnimatorKeyFrame : AvaloniaObject { public static readonly DirectProperty ValueProperty = - AvaloniaProperty.RegisterDirect(nameof(Value), k => k._value, (k, v) => k._value = v); + AvaloniaProperty.RegisterDirect(nameof(Value), k => k.Value, (k, v) => k.Value = v); public AnimatorKeyFrame() { From 2345818aab00359a51375157bbad978e66e1b079 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 27 Jul 2018 16:40:45 -0500 Subject: [PATCH 200/211] Change completion to be notified via a callback. Only notify that an animation is complete if all animators used in the animation are complete. --- src/Avalonia.Animation/Animation.cs | 57 ++++++++----------- .../AnimatorStateMachine`1.cs | 6 +- src/Avalonia.Animation/Animator`1.cs | 9 ++- src/Avalonia.Animation/IAnimation.cs | 4 +- src/Avalonia.Animation/IAnimator.cs | 2 +- .../Animation/TransformAnimator.cs | 6 +- 6 files changed, 39 insertions(+), 45 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 98fb652c4a..c228a49ec7 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -43,7 +43,6 @@ namespace Avalonia.Animation return null; } - private bool _isChildrenChanged = false; private List _subscription = new List(); public AvaloniaList _animators { get; set; } = new AvaloniaList(); @@ -77,16 +76,6 @@ namespace Avalonia.Animation /// public Easing Easing { get; set; } = new LinearEasing(); - /// - /// Triggers when the animation is completed. - /// - public event EventHandler Done; - - public Animation() - { - this.CollectionChanged += delegate { _isChildrenChanged = true; }; - } - private IList InterpretKeyframes(Animatable control) { var handlerList = new List<(Type type, AvaloniaProperty property)>(); @@ -152,11 +141,32 @@ namespace Avalonia.Animation } /// - public IDisposable Apply(Animatable control, IObservable matchObs) + public IDisposable Apply(Animatable control, IObservable match, Action onComplete) { - foreach (IAnimator animator in InterpretKeyframes(control)) + var animators = InterpretKeyframes(control); + if (animators.Count == 1) { - _subscription.Add(animator.Apply(this, control, matchObs)); + _subscription.Add(animators[0].Apply(this, control, match, onComplete)); + } + else + { + var completionTasks = onComplete != null ? new List() : null; + foreach (IAnimator animator in InterpretKeyframes(control)) + { + Action animatorOnComplete = null; + if (onComplete != null) + { + var tcs = new TaskCompletionSource(); + animatorOnComplete = () => tcs.SetResult(null); + completionTasks.Add(tcs.Task); + } + _subscription.Add(animator.Apply(this, control, match, animatorOnComplete)); + } + + if (onComplete != null) + { + Task.WhenAll(completionTasks).ContinueWith(_ => onComplete()); + } } return this; } @@ -169,26 +179,9 @@ namespace Avalonia.Animation if (this.RepeatCount == RepeatCount.Loop) run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); - EventHandler doneCallback = null; - doneCallback = (sender, args) => - { - if (sender == control) - { - run.SetResult(null); - this.Done -= doneCallback; - } - }; - - this.Done += doneCallback; - - this.Apply(control, Observable.Return(true)); + this.Apply(control, Observable.Return(true), () => run.SetResult(null)); return run.Task; } - - internal void SetDone(Animatable control) - { - Done?.Invoke(control, null); - } } } diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 4bffd5c145..87e189c997 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -35,6 +35,7 @@ namespace Avalonia.Animation private T _neutralValue; internal bool _unsubscribe = false; private IObserver _targetObserver; + private readonly Action _onComplete; [Flags] private enum KeyFramesStates @@ -51,7 +52,7 @@ namespace Avalonia.Animation Disposed } - public void Initialize(Animation animation, Animatable control, Animator animator) + public AnimatorStateMachine(Animation animation, Animatable control, Animator animator, Action onComplete) { _parent = animator; _targetAnimation = animation; @@ -82,6 +83,7 @@ namespace Avalonia.Animation _currentState = KeyFramesStates.DoDelay; else _currentState = KeyFramesStates.DoRun; + _onComplete = onComplete; } public void Step(PlayState _playState, Func Interpolator) @@ -245,7 +247,7 @@ namespace Avalonia.Animation } _targetObserver.OnCompleted(); - _targetAnimation.SetDone(_targetControl); + _onComplete?.Invoke(); Dispose(); handled = true; break; diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index a1eef87e1e..eb8b40647d 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -35,7 +35,7 @@ namespace Avalonia.Animation } /// - public virtual IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch) + public virtual IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch, Action onComplete) { if (!_isVerfifiedAndConverted) VerifyConvertKeyFrames(); @@ -45,7 +45,7 @@ namespace Avalonia.Animation .Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause) .Subscribe(_ => { - var timerObs = RunKeyFrames(animation, control); + var timerObs = RunKeyFrames(animation, control, onComplete); }); } @@ -97,10 +97,9 @@ namespace Avalonia.Animation /// /// Runs the KeyFrames Animation. /// - private IDisposable RunKeyFrames(Animation animation, Animatable control) + private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete) { - var stateMachine = new AnimatorStateMachine(); - stateMachine.Initialize(animation, control, this); + var stateMachine = new AnimatorStateMachine(animation, control, this, onComplete); Timing.AnimationStateTimer .TakeWhile(_ => !stateMachine._unsubscribe) diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index 734eb3e479..905d90fa52 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -13,11 +13,11 @@ namespace Avalonia.Animation /// /// Apply the animation to the specified control /// - IDisposable Apply(Animatable control, IObservable match); + IDisposable Apply(Animatable control, IObservable match, Action onComplete = null); /// /// Run the animation to the specified control /// Task RunAsync(Animatable control); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/IAnimator.cs b/src/Avalonia.Animation/IAnimator.cs index 6acca4d697..8b763db603 100644 --- a/src/Avalonia.Animation/IAnimator.cs +++ b/src/Avalonia.Animation/IAnimator.cs @@ -17,6 +17,6 @@ namespace Avalonia.Animation /// /// Applies the current KeyFrame group to the specified control. /// - IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch); + IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch, Action onComplete); } } diff --git a/src/Avalonia.Visuals/Animation/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/TransformAnimator.cs index 46cefbd061..61cac695b1 100644 --- a/src/Avalonia.Visuals/Animation/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/TransformAnimator.cs @@ -19,7 +19,7 @@ namespace Avalonia.Animation DoubleAnimator childKeyFrames; /// - public override IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch) + public override IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch, Action onComplete) { var ctrl = (Visual)control; @@ -51,7 +51,7 @@ namespace Avalonia.Animation // It's a transform object so let's target that. if (renderTransformType == Property.OwnerType) { - return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch); + return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch, onComplete); } // It's a TransformGroup and try finding the target there. else if (renderTransformType == typeof(TransformGroup)) @@ -60,7 +60,7 @@ namespace Avalonia.Animation { if (transform.GetType() == Property.OwnerType) { - return childKeyFrames.Apply(animation, transform, obsMatch); + return childKeyFrames.Apply(animation, transform, obsMatch, onComplete); } } } From 7d1b7593a5572bbc2b74ac3e55c42065a9ba4852 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 2 Aug 2018 15:31:34 -0500 Subject: [PATCH 201/211] Dispose subscriptions on completed code-behind run of animation. --- src/Avalonia.Animation/Animation.cs | 39 +++++++++++++---------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index c228a49ec7..56bc667f8a 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -12,13 +12,14 @@ using System.Reflection; using System.Linq; using System.Threading.Tasks; using System.Reactive.Linq; +using System.Reactive.Disposables; namespace Avalonia.Animation { /// /// Tracks the progress of an animation. /// - public class Animation : AvaloniaList, IDisposable, IAnimation + public class Animation : AvaloniaList, IAnimation { private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)> { @@ -43,7 +44,6 @@ namespace Avalonia.Animation return null; } - private List _subscription = new List(); public AvaloniaList _animators { get; set; } = new AvaloniaList(); /// @@ -76,10 +76,11 @@ namespace Avalonia.Animation /// public Easing Easing { get; set; } = new LinearEasing(); - private IList InterpretKeyframes(Animatable control) + private (IList Animators, IList subscriptions) InterpretKeyframes(Animatable control) { var handlerList = new List<(Type type, AvaloniaProperty property)>(); var animatorKeyFrames = new List(); + var subscriptions = new List(); foreach (var keyframe in this) { @@ -104,7 +105,7 @@ namespace Avalonia.Animation var newKF = new AnimatorKeyFrame(handler, cue); - _subscription.Add(newKF.BindSetter(setter, control)); + subscriptions.Add(newKF.BindSetter(setter, control)); animatorKeyFrames.Add(newKF); } @@ -126,32 +127,21 @@ namespace Avalonia.Animation animator.Add(keyframe); } - return newAnimatorInstances; - } - - /// - /// Cancels the animation. - /// - public void Dispose() - { - foreach (var sub in _subscription) - { - sub.Dispose(); - } + return (newAnimatorInstances, subscriptions); } /// public IDisposable Apply(Animatable control, IObservable match, Action onComplete) { - var animators = InterpretKeyframes(control); + var (animators, subscriptions) = InterpretKeyframes(control); if (animators.Count == 1) { - _subscription.Add(animators[0].Apply(this, control, match, onComplete)); + subscriptions.Add(animators[0].Apply(this, control, match, onComplete)); } else { var completionTasks = onComplete != null ? new List() : null; - foreach (IAnimator animator in InterpretKeyframes(control)) + foreach (IAnimator animator in animators) { Action animatorOnComplete = null; if (onComplete != null) @@ -160,7 +150,7 @@ namespace Avalonia.Animation animatorOnComplete = () => tcs.SetResult(null); completionTasks.Add(tcs.Task); } - _subscription.Add(animator.Apply(this, control, match, animatorOnComplete)); + subscriptions.Add(animator.Apply(this, control, match, animatorOnComplete)); } if (onComplete != null) @@ -168,7 +158,7 @@ namespace Avalonia.Animation Task.WhenAll(completionTasks).ContinueWith(_ => onComplete()); } } - return this; + return new CompositeDisposable(subscriptions); } /// @@ -179,7 +169,12 @@ namespace Avalonia.Animation if (this.RepeatCount == RepeatCount.Loop) run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); - this.Apply(control, Observable.Return(true), () => run.SetResult(null)); + IDisposable subscriptions = null; + subscriptions = this.Apply(control, Observable.Return(true), () => + { + run.SetResult(null); + subscriptions.Dispose(); + }); return run.Task; } From 5e4555e36637e8e9b031d1ea63bed3b14f9f0e36 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 2 Aug 2018 15:45:27 -0500 Subject: [PATCH 202/211] Add conditional access to satisfy our inspector. --- src/Avalonia.Animation/Animation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 56bc667f8a..3dc5b5c71a 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -173,7 +173,7 @@ namespace Avalonia.Animation subscriptions = this.Apply(control, Observable.Return(true), () => { run.SetResult(null); - subscriptions.Dispose(); + subscriptions?.Dispose(); }); return run.Task; From 4e49c896640d3d73cb3e6edc944efe117ce28c1e Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Fri, 3 Aug 2018 15:52:35 +0100 Subject: [PATCH 203/211] Revert "dont highlight selected menuitems." --- src/Avalonia.Themes.Default/MenuItem.xaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 47b4528f88..53965db016 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -122,6 +122,11 @@ + +