From eb6bfd3de86dc8e5c909c5eb51c86e998f7c76c4 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 6 Jun 2017 00:46:47 +0300 Subject: [PATCH 001/149] 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/149] 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/149] 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/149] 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/149] 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/149] 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/149] 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/149] 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/149] 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 2c53bf4750618490031e0e6cb3c5ddc3810f137e Mon Sep 17 00:00:00 2001 From: boombuler Date: Mon, 7 May 2018 07:58:31 +0200 Subject: [PATCH 010/149] send the modifiers with drag events --- .../Platform/InProcessDragSource.cs | 2 +- src/Avalonia.Input/DragDropDevice.cs | 24 +++++------ src/Avalonia.Input/DragEventArgs.cs | 5 ++- src/Avalonia.Input/Raw/RawDragEvent.cs | 4 +- src/OSX/Avalonia.MonoMac/TopLevelImpl.cs | 4 +- .../Interop/UnmanagedMethods.cs | 6 +-- src/Windows/Avalonia.Win32/OleDragSource.cs | 9 ++-- src/Windows/Avalonia.Win32/OleDropTarget.cs | 43 +++++++++++++++---- 8 files changed, 65 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs index e136efe2a9..adbd1632b7 100644 --- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs +++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs @@ -60,7 +60,7 @@ namespace Avalonia.Platform { _lastPosition = pt; - RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, _allowedEffects); + RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, _allowedEffects, modifiers); var tl = root.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); tl.PlatformImpl.Input(rawEvent); diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Input/DragDropDevice.cs index 9fb100371f..0692b21c66 100644 --- a/src/Avalonia.Input/DragDropDevice.cs +++ b/src/Avalonia.Input/DragDropDevice.cs @@ -19,11 +19,11 @@ namespace Avalonia.Input return null; } - private DragDropEffects RaiseDragEvent(Interactive target, IInputElement inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data) + private DragDropEffects RaiseDragEvent(Interactive target, IInputElement inputRoot, Point point, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data, InputModifiers modifiers) { if (target == null) return DragDropEffects.None; - var args = new DragEventArgs(routedEvent, data, target, inputRoot.TranslatePoint(point, target)) + var args = new DragEventArgs(routedEvent, data, target, inputRoot.TranslatePoint(point, target), modifiers) { RoutedEvent = routedEvent, DragEffects = operation @@ -32,24 +32,24 @@ namespace Avalonia.Input return args.DragEffects; } - private DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) + private DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) { _lastTarget = GetTarget(inputRoot, point); - return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragEnterEvent, effects, data); + return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers); } - private DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) + private DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) { var target = GetTarget(inputRoot, point); if (target == _lastTarget) - return RaiseDragEvent(target, inputRoot, point, DragDrop.DragOverEvent, effects, data); + return RaiseDragEvent(target, inputRoot, point, DragDrop.DragOverEvent, effects, data, modifiers); try { if (_lastTarget != null) _lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent)); - return RaiseDragEvent(target, inputRoot, point, DragDrop.DragEnterEvent, effects, data); + return RaiseDragEvent(target, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers); } finally { @@ -71,11 +71,11 @@ namespace Avalonia.Input } } - private DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) + private DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects, InputModifiers modifiers) { try { - return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DropEvent, effects, data); + return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DropEvent, effects, data, modifiers); } finally { @@ -94,16 +94,16 @@ namespace Avalonia.Input switch (e.Type) { case RawDragEventType.DragEnter: - e.Effects = DragEnter(e.InputRoot, e.Location, e.Data, e.Effects); + e.Effects = DragEnter(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers); break; case RawDragEventType.DragOver: - e.Effects = DragOver(e.InputRoot, e.Location, e.Data, e.Effects); + e.Effects = DragOver(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers); break; case RawDragEventType.DragLeave: DragLeave(e.InputRoot); break; case RawDragEventType.Drop: - e.Effects = Drop(e.InputRoot, e.Location, e.Data, e.Effects); + e.Effects = Drop(e.InputRoot, e.Location, e.Data, e.Effects, e.Modifiers); break; } } diff --git a/src/Avalonia.Input/DragEventArgs.cs b/src/Avalonia.Input/DragEventArgs.cs index 669fd846a1..915ee4ee5c 100644 --- a/src/Avalonia.Input/DragEventArgs.cs +++ b/src/Avalonia.Input/DragEventArgs.cs @@ -13,6 +13,8 @@ namespace Avalonia.Input public IDataObject Data { get; private set; } + public InputModifiers Modifiers { get; private set; } + public Point GetPosition(IVisual relativeTo) { var point = new Point(0, 0); @@ -29,12 +31,13 @@ namespace Avalonia.Input return point; } - public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation) + public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, InputModifiers modifiers) : base(routedEvent) { this.Data = data; this._target = target; this._targetLocation = targetLocation; + this.Modifiers = modifiers; } } diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs index 49125b4c07..80653b4873 100644 --- a/src/Avalonia.Input/Raw/RawDragEvent.cs +++ b/src/Avalonia.Input/Raw/RawDragEvent.cs @@ -11,9 +11,10 @@ namespace Avalonia.Input.Raw public IDataObject Data { get; } public DragDropEffects Effects { get; set; } public RawDragEventType Type { get; } + public InputModifiers Modifiers { get; } public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, - IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects) + IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects, InputModifiers modifiers) :base(inputDevice, 0) { Type = type; @@ -21,6 +22,7 @@ namespace Avalonia.Input.Raw Location = location; Data = data; Effects = effects; + Modifiers = modifiers; } } } \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs index db7f29f05b..f661cc0450 100644 --- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs +++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs @@ -164,8 +164,10 @@ namespace Avalonia.MonoMac var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask); DraggingInfo info = new DraggingInfo(sender); + var pt = TranslateLocalPoint(info.Location); - var args = new RawDragEvent(dragDevice, type, root, pt, info, dragOp); + // TODO: Find a way to obtain the InputModifiers... + var args = new RawDragEvent(dragDevice, type, root, pt, info, dragOp, InputModifiers.None); input(args); return DraggingInfo.ConvertDragOperation(args.Effects); } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 5c24aa1c69..b115f2af4e 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -218,6 +218,8 @@ namespace Avalonia.Win32.Interop MK_SHIFT = 0x0004, + MK_ALT = 0x0020, + MK_XBUTTON1 = 0x0020, MK_XBUTTON2 = 0x0040 @@ -1381,9 +1383,7 @@ namespace Avalonia.Win32.Interop Link = 4, Scroll = -2147483648, } - - - + [ComImport] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("00000122-0000-0000-C000-000000000046")] diff --git a/src/Windows/Avalonia.Win32/OleDragSource.cs b/src/Windows/Avalonia.Win32/OleDragSource.cs index 522014abc0..a87995952a 100644 --- a/src/Windows/Avalonia.Win32/OleDragSource.cs +++ b/src/Windows/Avalonia.Win32/OleDragSource.cs @@ -11,10 +11,11 @@ namespace Avalonia.Win32 private const int DRAGDROP_S_DROP = 0x00040100; private const int DRAGDROP_S_CANCEL = 0x00040101; - private const int KEYSTATE_LEFTMB = 1; - private const int KEYSTATE_MIDDLEMB = 16; - private const int KEYSTATE_RIGHTMB = 2; - private static readonly int[] MOUSE_BUTTONS = new int[] { KEYSTATE_LEFTMB, KEYSTATE_MIDDLEMB, KEYSTATE_RIGHTMB }; + private static readonly int[] MOUSE_BUTTONS = new int[] { + (int)UnmanagedMethods.ModifierKeys.MK_LBUTTON, + (int)UnmanagedMethods.ModifierKeys.MK_MBUTTON, + (int)UnmanagedMethods.ModifierKeys.MK_RBUTTON + }; public int QueryContinueDrag(int fEscapePressed, int grfKeyState) { diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index 973564a3d1..6a10cc7e98 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -45,6 +45,26 @@ namespace Avalonia.Win32 return result; } + private static InputModifiers ConvertKeyState(int grfKeyState) + { + InputModifiers modifiers = InputModifiers.None; + var state = (UnmanagedMethods.ModifierKeys)grfKeyState; + + if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_LBUTTON)) + modifiers |= InputModifiers.LeftMouseButton; + if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_MBUTTON)) + modifiers |= InputModifiers.MiddleMouseButton; + if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_RBUTTON)) + modifiers |= InputModifiers.RightMouseButton; + if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_SHIFT)) + modifiers |= InputModifiers.Shift; + if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_CONTROL)) + modifiers |= InputModifiers.Control; + if (state.HasFlag(UnmanagedMethods.ModifierKeys.MK_ALT)) + modifiers |= InputModifiers.Alt; + return modifiers; + } + UnmanagedMethods.HRESULT IDropTarget.DragEnter(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect) { var dispatch = _tl?.Input; @@ -56,13 +76,15 @@ namespace Avalonia.Win32 _currentDrag = pDataObj as IDataObject; if (_currentDrag == null) _currentDrag = new OleDataObject(pDataObj); + var args = new RawDragEvent( _dragDevice, RawDragEventType.DragEnter, _target, GetDragLocation(pt), _currentDrag, - ConvertDropEffect(pdwEffect) + ConvertDropEffect(pdwEffect), + ConvertKeyState(grfKeyState) ); dispatch(args); pdwEffect = ConvertDropEffect(args.Effects); @@ -85,7 +107,8 @@ namespace Avalonia.Win32 _target, GetDragLocation(pt), _currentDrag, - ConvertDropEffect(pdwEffect) + ConvertDropEffect(pdwEffect), + ConvertKeyState(grfKeyState) ); dispatch(args); pdwEffect = ConvertDropEffect(args.Effects); @@ -98,12 +121,13 @@ namespace Avalonia.Win32 try { _tl?.Input(new RawDragEvent( - _dragDevice, - RawDragEventType.DragLeave, - _target, - default(Point), - null, - DragDropEffects.None + _dragDevice, + RawDragEventType.DragLeave, + _target, + default(Point), + null, + DragDropEffects.None, + InputModifiers.None )); return UnmanagedMethods.HRESULT.S_OK; } @@ -134,7 +158,8 @@ namespace Avalonia.Win32 _target, GetDragLocation(pt), _currentDrag, - ConvertDropEffect(pdwEffect) + ConvertDropEffect(pdwEffect), + ConvertKeyState(grfKeyState) ); dispatch(args); pdwEffect = ConvertDropEffect(args.Effects); From e7d44181b9da285ff5cf19a6ecd172a960e7cf3b Mon Sep 17 00:00:00 2001 From: Karnah Date: Fri, 11 May 2018 12:04:38 +0500 Subject: [PATCH 011/149] 1448: Fixed Center and Right alignment selection text --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 29 ++++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index f22722a0b5..923fe896f9 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -36,8 +36,8 @@ namespace Avalonia.Skia //Paint.TextEncoding = SKTextEncoding.Utf8; _paint.TextEncoding = SKTextEncoding.Utf16; _paint.IsStroke = false; - _paint.IsAntialias = true; - _paint.LcdRenderText = true; + _paint.IsAntialias = true; + _paint.LcdRenderText = true; _paint.SubpixelText = true; _paint.Typeface = skiaTypeface; _paint.TextSize = (float)(typeface?.FontSize ?? 12); @@ -226,7 +226,26 @@ namespace Avalonia.Skia { float currX = x; string subStr; + float measure; int len; + float factor; + switch (paint.TextAlign) + { + case SKTextAlign.Left: + factor = 0; + break; + case SKTextAlign.Center: + factor = 0.5f; + break; + case SKTextAlign.Right: + factor = 1; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + var textLine = Text.Substring(line.Start, line.Length); + currX -= paint.MeasureText(textLine) * factor; for (int i = line.Start; i < line.Start + line.Length;) { @@ -244,13 +263,15 @@ namespace Avalonia.Skia } subStr = Text.Substring(i, len); + measure = paint.MeasureText(subStr); + currX += measure * factor; ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering); - + canvas.DrawText(subStr, currX, origin.Y + line.Top + _lineOffset, paint); i += len; - currX += paint.MeasureText(subStr); + currX += measure * (1 - factor); } } } From 903bdf9035063ebf803b23674191323eb4a75e07 Mon Sep 17 00:00:00 2001 From: Karnah Date: Fri, 11 May 2018 12:20:24 +0500 Subject: [PATCH 012/149] Fix AvaloniaFormattedTextLine line width --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 923fe896f9..7d39c71b2f 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -559,11 +559,11 @@ namespace Avalonia.Skia measured = LineBreak(Text, curOff, length, _paint, constraint, out trailingnumber); AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine(); + line.Start = curOff; line.TextLength = measured; subString = Text.Substring(line.Start, line.TextLength); lineWidth = _paint.MeasureText(subString); - line.Start = curOff; line.Length = measured - trailingnumber; line.Width = lineWidth; line.Height = _lineHeight; From 07b457d8a0021ada606699529c9ce7c42f234c1c Mon Sep 17 00:00:00 2001 From: Nelson Carrillo Date: Tue, 29 May 2018 22:34:32 -0400 Subject: [PATCH 013/149] Add support for locating custom renderers --- src/Avalonia.Visuals/Media/DrawingContext.cs | 2 +- .../Rendering/IRendererFactory.cs | 20 +++++++++++++++++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 9 ++++++--- 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 src/Avalonia.Visuals/Rendering/IRendererFactory.cs diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 1d25224b8d..962f2c1ba8 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -59,7 +59,7 @@ namespace Avalonia.Media //HACK: This is a temporary hack that is used in the render loop //to update TransformedBounds property [Obsolete("HACK for render loop, don't use")] - internal Matrix CurrentContainerTransform => _currentContainerTransform; + public Matrix CurrentContainerTransform => _currentContainerTransform; /// /// Draws a bitmap image. diff --git a/src/Avalonia.Visuals/Rendering/IRendererFactory.cs b/src/Avalonia.Visuals/Rendering/IRendererFactory.cs new file mode 100644 index 0000000000..0d7044e125 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/IRendererFactory.cs @@ -0,0 +1,20 @@ +// 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; + +namespace Avalonia.Rendering +{ + /// + /// Defines the interface for a renderer factory. + /// + public interface IRendererFactory + { + /// + /// Creates a renderer. + /// + /// The root visual. + /// The render loop. + IRenderer Create(IRenderRoot root, IRenderLoop renderLoop); + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 7b9f8ee066..159c8386b6 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -124,9 +124,12 @@ namespace Avalonia.Win32 public IRenderer CreateRenderer(IRenderRoot root) { var loop = AvaloniaLocator.Current.GetService(); - return Win32Platform.UseDeferredRendering ? - (IRenderer)new DeferredRenderer(root, loop) : - new ImmediateRenderer(root); + var customRendererFactory = AvaloniaLocator.Current.GetService(); + + if (customRendererFactory != null) + return customRendererFactory.Create(root, loop); + + return Win32Platform.UseDeferredRendering ? (IRenderer)new DeferredRenderer(root, loop) : new ImmediateRenderer(root); } public void Resize(Size value) From f596cb13bb369dd27ffba3fea74d6f857748f2f7 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 31 May 2018 21:45:04 -0500 Subject: [PATCH 014/149] Don't set when value is equal to the last fetched value (invalidated on PropertyChanged event). --- .../Plugins/InpcPropertyAccessorPlugin.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index ba4e60eb74..c03a384eee 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -52,9 +52,11 @@ namespace Avalonia.Data.Core.Plugins private class Accessor : PropertyAccessorBase, IWeakSubscriber { + private static readonly object CacheInvalid = new object(); private readonly WeakReference _reference; private readonly PropertyInfo _property; private bool _eventRaised; + private object _lastValue = CacheInvalid; public Accessor(WeakReference reference, PropertyInfo property) { @@ -72,7 +74,7 @@ namespace Avalonia.Data.Core.Plugins get { var o = _reference.Target; - return (o != null) ? _property.GetValue(o) : null; + return (_lastValue = (o != null) ? _property.GetValue(o) : null); } } @@ -80,6 +82,11 @@ namespace Avalonia.Data.Core.Plugins { if (_property.CanWrite) { + if (!ShouldSet(value)) + { + return true; + } + _eventRaised = false; _property.SetValue(_reference.Target, value); @@ -94,11 +101,24 @@ namespace Avalonia.Data.Core.Plugins return false; } + private bool ShouldSet(object value) + { + if (PropertyType.IsValueType) + { + return !_lastValue.Equals(value); + } + else + { + return !Object.ReferenceEquals(_lastValue, value); + } + } + void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) { _eventRaised = true; + _lastValue = CacheInvalid; SendCurrentValue(); } } From 41b20f63f0db3b3a63a772e70835336ad3f29d67 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 31 May 2018 22:18:44 -0500 Subject: [PATCH 015/149] Move fix up to PropertyAccessorNode from InpcPropertyAccessorPlugin. --- .../Plugins/InpcPropertyAccessorPlugin.cs | 22 +---------- .../Data/Core/PropertyAccessorNode.cs | 37 ++++++++++++++++++- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index c03a384eee..ba4e60eb74 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -52,11 +52,9 @@ namespace Avalonia.Data.Core.Plugins private class Accessor : PropertyAccessorBase, IWeakSubscriber { - private static readonly object CacheInvalid = new object(); private readonly WeakReference _reference; private readonly PropertyInfo _property; private bool _eventRaised; - private object _lastValue = CacheInvalid; public Accessor(WeakReference reference, PropertyInfo property) { @@ -74,7 +72,7 @@ namespace Avalonia.Data.Core.Plugins get { var o = _reference.Target; - return (_lastValue = (o != null) ? _property.GetValue(o) : null); + return (o != null) ? _property.GetValue(o) : null; } } @@ -82,11 +80,6 @@ namespace Avalonia.Data.Core.Plugins { if (_property.CanWrite) { - if (!ShouldSet(value)) - { - return true; - } - _eventRaised = false; _property.SetValue(_reference.Target, value); @@ -101,24 +94,11 @@ namespace Avalonia.Data.Core.Plugins return false; } - private bool ShouldSet(object value) - { - if (PropertyType.IsValueType) - { - return !_lastValue.Equals(value); - } - else - { - return !Object.ReferenceEquals(_lastValue, value); - } - } - void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) { _eventRaised = true; - _lastValue = CacheInvalid; SendCurrentValue(); } } diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 4dbff4602f..efa5b88303 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -12,8 +12,10 @@ namespace Avalonia.Data.Core { internal class PropertyAccessorNode : ExpressionNode, ISettableNode { + private static readonly object CacheInvalid = new object(); private readonly bool _enableValidation; private IPropertyAccessor _accessor; + private object _lastValue = CacheInvalid; public PropertyAccessorNode(string propertyName, bool enableValidation) { @@ -29,12 +31,32 @@ namespace Avalonia.Data.Core { if (_accessor != null) { - try { return _accessor.SetValue(value, priority); } catch { } + try + { + if (ShouldNotSet(value)) + { + return true; + } + else + { + return _accessor.SetValue(value, priority); + } + } + catch { } } return false; } + private bool ShouldNotSet(object value) + { + if (PropertyType.IsValueType) + { + return _lastValue.Equals(value); + } + return Object.ReferenceEquals(_lastValue, value); + } + protected override IObservable StartListeningCore(WeakReference reference) { var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); @@ -58,7 +80,18 @@ namespace Avalonia.Data.Core _accessor = accessor; return Disposable.Create(() => _accessor = null); }, - _ => accessor); + _ => accessor).Select(value => + { + if (value is BindingNotification notification) + { + _lastValue = notification.HasValue ? notification.Value : CacheInvalid; + } + else + { + _lastValue = value; + } + return value; + }); } } } From a37e24dc441f471f8f9d1896b09b5be569fc5028 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 2 Jun 2018 12:30:12 -0500 Subject: [PATCH 016/149] Add unit test. --- .../AvaloniaObjectTests_Binding.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 02fb1f11ad..65d37503b6 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -457,6 +457,17 @@ namespace Avalonia.Base.UnitTests Assert.True(target.IsAnimating(Class1.FooProperty)); } + [Fact] + public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation() + { + var target = new Class1(); + var source = new TestTwoWayBindingViewModel(); + + target.Bind(Class1.DoubleValueProperty, new Binding(nameof(source.Value), BindingMode.TwoWay) { Source = source }); + + Assert.False(source.SetterCalled); + } + /// /// Returns an observable that returns a single value but does not complete. /// @@ -545,5 +556,22 @@ namespace Avalonia.Base.UnitTests } } } + + private class TestTwoWayBindingViewModel + { + private double _value; + + public double Value + { + get => _value; + set + { + _value = value; + SetterCalled = true; + } + } + + public bool SetterCalled { get; private set; } + } } } \ No newline at end of file From 5ea9677cb961bbe3a0a06443eb516daf8fd86aa2 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 2 Jun 2018 12:30:49 -0500 Subject: [PATCH 017/149] Use WeakReferences to make sure we aren't accidentally keeping objects alive in this new cache. --- src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index efa5b88303..c958cfdb25 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -15,7 +15,7 @@ namespace Avalonia.Data.Core private static readonly object CacheInvalid = new object(); private readonly bool _enableValidation; private IPropertyAccessor _accessor; - private object _lastValue = CacheInvalid; + private WeakReference _lastValue = null; public PropertyAccessorNode(string propertyName, bool enableValidation) { @@ -52,9 +52,9 @@ namespace Avalonia.Data.Core { if (PropertyType.IsValueType) { - return _lastValue.Equals(value); + return _lastValue?.Target.Equals(value) ?? false; } - return Object.ReferenceEquals(_lastValue, value); + return Object.ReferenceEquals(_lastValue?.Target ?? CacheInvalid, value); } protected override IObservable StartListeningCore(WeakReference reference) @@ -84,11 +84,11 @@ namespace Avalonia.Data.Core { if (value is BindingNotification notification) { - _lastValue = notification.HasValue ? notification.Value : CacheInvalid; + _lastValue = notification.HasValue ? new WeakReference(notification.Value) : null; } else { - _lastValue = value; + _lastValue = new WeakReference(value); } return value; }); From 1c3b714a0ec25166a8f3b15291f432b09da9b883 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 2 Jun 2018 13:02:13 -0500 Subject: [PATCH 018/149] Move fix up to SettableNode and ExpressionNode so all settable node types (i.e. PropertyAccessor and Indexer nodes) can get the fix. --- src/Avalonia.Base/Data/Core/ExpressionNode.cs | 6 +++ .../Data/Core/ExpressionObserver.cs | 4 +- src/Avalonia.Base/Data/Core/ISettableNode.cs | 15 ------- src/Avalonia.Base/Data/Core/IndexerNode.cs | 6 +-- .../Data/Core/PropertyAccessorNode.cs | 44 ++++--------------- src/Avalonia.Base/Data/Core/SettableNode.cs | 38 ++++++++++++++++ .../AvaloniaObjectTests_Binding.cs | 21 +++++++++ 7 files changed, 79 insertions(+), 55 deletions(-) delete mode 100644 src/Avalonia.Base/Data/Core/ISettableNode.cs create mode 100644 src/Avalonia.Base/Data/Core/SettableNode.cs diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index ae70cacdba..ac7e97a4b1 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -11,6 +11,7 @@ namespace Avalonia.Data.Core { internal abstract class ExpressionNode : ISubject { + private static readonly object CacheInvalid = new object(); protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); @@ -18,6 +19,8 @@ namespace Avalonia.Data.Core private IDisposable _valueSubscription; private IObserver _observer; + protected WeakReference LastValue { get; private set; } + public abstract string Description { get; } public ExpressionNode Next { get; set; } @@ -61,6 +64,7 @@ namespace Avalonia.Data.Core { _valueSubscription?.Dispose(); _valueSubscription = null; + LastValue = null; nextSubscription?.Dispose(); _observer = null; }); @@ -120,6 +124,7 @@ namespace Avalonia.Data.Core if (notification == null) { + LastValue = new WeakReference(value); if (Next != null) { Next.Target = new WeakReference(value); @@ -131,6 +136,7 @@ namespace Avalonia.Data.Core } else { + LastValue = new WeakReference(notification.Value); if (Next != null) { Next.Target = new WeakReference(notification.Value); diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 7719f93a02..14bc09f5b7 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -154,7 +154,7 @@ namespace Avalonia.Data.Core /// public bool SetValue(object value, BindingPriority priority = BindingPriority.LocalValue) { - if (Leaf is ISettableNode settable) + if (Leaf is SettableNode settable) { var node = _node; while (node != null) @@ -188,7 +188,7 @@ namespace Avalonia.Data.Core /// Gets the type of the expression result or null if the expression could not be /// evaluated. /// - public Type ResultType => (Leaf as ISettableNode)?.PropertyType; + public Type ResultType => (Leaf as SettableNode)?.PropertyType; /// /// Gets the leaf node. diff --git a/src/Avalonia.Base/Data/Core/ISettableNode.cs b/src/Avalonia.Base/Data/Core/ISettableNode.cs deleted file mode 100644 index 7788407833..0000000000 --- a/src/Avalonia.Base/Data/Core/ISettableNode.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Avalonia.Data; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Avalonia.Data.Core -{ - interface ISettableNode - { - bool SetTargetValue(object value, BindingPriority priority); - Type PropertyType { get; } - } -} diff --git a/src/Avalonia.Base/Data/Core/IndexerNode.cs b/src/Avalonia.Base/Data/Core/IndexerNode.cs index 47e82fa2d3..633d3558ee 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNode.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNode.cs @@ -15,7 +15,7 @@ using Avalonia.Data; namespace Avalonia.Data.Core { - internal class IndexerNode : ExpressionNode, ISettableNode + internal class IndexerNode : SettableNode { public IndexerNode(IList arguments) { @@ -52,7 +52,7 @@ namespace Avalonia.Data.Core return Observable.Merge(inputs).StartWith(GetValue(target)); } - public bool SetTargetValue(object value, BindingPriority priority) + protected override bool SetTargetValueCore(object value, BindingPriority priority) { var typeInfo = Target.Target.GetType().GetTypeInfo(); var list = Target.Target as IList; @@ -154,7 +154,7 @@ namespace Avalonia.Data.Core 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) { diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index c958cfdb25..9d657b3144 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -10,12 +10,10 @@ using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core { - internal class PropertyAccessorNode : ExpressionNode, ISettableNode + internal class PropertyAccessorNode : SettableNode { - private static readonly object CacheInvalid = new object(); private readonly bool _enableValidation; private IPropertyAccessor _accessor; - private WeakReference _lastValue = null; public PropertyAccessorNode(string propertyName, bool enableValidation) { @@ -25,22 +23,15 @@ namespace Avalonia.Data.Core public override string Description => PropertyName; public string PropertyName { get; } - public Type PropertyType => _accessor?.PropertyType; + public override Type PropertyType => _accessor?.PropertyType; - public bool SetTargetValue(object value, BindingPriority priority) + protected override bool SetTargetValueCore(object value, BindingPriority priority) { if (_accessor != null) { try { - if (ShouldNotSet(value)) - { - return true; - } - else - { - return _accessor.SetValue(value, priority); - } + return _accessor.SetValue(value, priority); } catch { } } @@ -48,15 +39,6 @@ namespace Avalonia.Data.Core return false; } - private bool ShouldNotSet(object value) - { - if (PropertyType.IsValueType) - { - return _lastValue?.Target.Equals(value) ?? false; - } - return Object.ReferenceEquals(_lastValue?.Target ?? CacheInvalid, value); - } - protected override IObservable StartListeningCore(WeakReference reference) { var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); @@ -78,20 +60,12 @@ namespace Avalonia.Data.Core () => { _accessor = accessor; - return Disposable.Create(() => _accessor = null); - }, - _ => accessor).Select(value => - { - if (value is BindingNotification notification) + return Disposable.Create(() => { - _lastValue = notification.HasValue ? new WeakReference(notification.Value) : null; - } - else - { - _lastValue = new WeakReference(value); - } - return value; - }); + _accessor = null; + }); + }, + _ => accessor); } } } diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs new file mode 100644 index 0000000000..1f6ec0bff4 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -0,0 +1,38 @@ +using Avalonia.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Data.Core +{ + internal abstract class SettableNode : ExpressionNode + { + public bool SetTargetValue(object value, BindingPriority priority) + { + if (ShouldNotSet(value)) + { + return true; + } + return SetTargetValueCore(value, priority); + } + + private bool ShouldNotSet(object value) + { + if (PropertyType == null) + { + return false; + } + if (PropertyType.IsValueType) + { + return LastValue?.Target.Equals(value) ?? false; + } + return LastValue != null && Object.ReferenceEquals(LastValue?.Target, value); + } + + protected abstract bool SetTargetValueCore(object value, BindingPriority priority); + + public abstract Type PropertyType { get; } + } +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 65d37503b6..4638aa84a5 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -468,6 +468,17 @@ namespace Avalonia.Base.UnitTests Assert.False(source.SetterCalled); } + [Fact] + public void TwoWay_Binding_Should_Not_Call_Setter_On_Creation_Indexer() + { + var target = new Class1(); + var source = new TestTwoWayBindingViewModel(); + + target.Bind(Class1.DoubleValueProperty, new Binding("[0]", BindingMode.TwoWay) { Source = source }); + + Assert.False(source.SetterCalled); + } + /// /// Returns an observable that returns a single value but does not complete. /// @@ -571,6 +582,16 @@ namespace Avalonia.Base.UnitTests } } + public double this[int index] + { + get => _value; + set + { + _value = value; + SetterCalled = true; + } + } + public bool SetterCalled { get; private set; } } } From f423dbbb1de22222abf01bdfdcd9f7fec5c20d8c Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 2 Jun 2018 15:45:05 -0500 Subject: [PATCH 019/149] Fix codepath where the property is a value type but the weakreference has been collected. --- src/Avalonia.Base/Data/Core/SettableNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index 1f6ec0bff4..092cdbe48f 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -26,7 +26,7 @@ namespace Avalonia.Data.Core } if (PropertyType.IsValueType) { - return LastValue?.Target.Equals(value) ?? false; + return LastValue?.Target != null && LastValue.Target.Equals(value); } return LastValue != null && Object.ReferenceEquals(LastValue?.Target, value); } From 0af6dd2c0a0361012220bcccb9febddd37d4531a Mon Sep 17 00:00:00 2001 From: CommonGuy Date: Mon, 4 Jun 2018 19:55:31 +0200 Subject: [PATCH 020/149] Implement topmost on window --- src/Avalonia.Controls/Platform/IWindowImpl.cs | 5 +++++ src/Avalonia.Controls/Window.cs | 14 ++++++++++++++ .../Remote/PreviewerWindowImpl.cs | 4 ++++ src/Avalonia.DesignerSupport/Remote/Stubs.cs | 4 ++++ src/Gtk/Avalonia.Gtk3/Interop/Native.cs | 6 +++++- src/Gtk/Avalonia.Gtk3/WindowImpl.cs | 4 +++- src/OSX/Avalonia.MonoMac/WindowImpl.cs | 5 +++++ .../Avalonia.Win32/Interop/UnmanagedMethods.cs | 8 ++++++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 17 +++++++++++++++++ 9 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index f1f3925133..093e9bc57c 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -55,6 +55,11 @@ namespace Avalonia.Platform /// void CanResize(bool value); + /// + /// Gets or sets whether this window appears on top of all other windows + /// + void SetTopmost(bool value); + /// /// Gets or sets a method called before the underlying implementation is destroyed. /// Return true to prevent the underlying implementation from closing. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 3cbfdbd657..1943225b00 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -104,6 +104,9 @@ namespace Avalonia.Controls public static readonly StyledProperty CanResizeProperty = AvaloniaProperty.Register(nameof(CanResize), true); + public static readonly StyledProperty TopmostProperty = + AvaloniaProperty.Register(nameof(Topmost)); + private readonly NameScope _nameScope = new NameScope(); private object _dialogResult; private readonly Size _maxPlatformClientSize; @@ -125,6 +128,8 @@ namespace Avalonia.Controls CanResizeProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.CanResize((bool)e.NewValue)); + TopmostProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetTopmost((bool)e.NewValue)); + WindowStateProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); } @@ -230,6 +235,15 @@ namespace Avalonia.Controls set { SetValue(CanResizeProperty, value); } } + /// + /// Gets or sets whether this window appears on top of all other windows + /// + public bool Topmost + { + get { return GetValue(TopmostProperty); } + set { SetValue(TopmostProperty, value); } + } + /// /// Gets or sets the icon of the window. /// diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index ef16d06b60..d9a30f0d29 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -102,5 +102,9 @@ namespace Avalonia.DesignerSupport.Remote public void CanResize(bool value) { } + + public void SetTopmost(bool value) + { + } } } \ No newline at end of file diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index e749d10468..43f0e0a95e 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -104,6 +104,10 @@ namespace Avalonia.DesignerSupport.Remote public void CanResize(bool value) { } + + public void SetTopmost(bool value) + { + } } class ClipboardStub : IClipboard diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs index 1adaf9f4e1..9cc6ba6901 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -261,10 +261,13 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_unmaximize(GtkWindow window); - + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_close(GtkWindow window); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_set_keep_above(GtkWindow gtkWindow, bool setting); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_set_geometry_hints(GtkWindow window, IntPtr geometry_widget, ref GdkGeometry geometry, GdkWindowHints geom_mask); @@ -472,6 +475,7 @@ namespace Avalonia.Gtk3.Interop public static D.gtk_window_maximize GtkWindowMaximize; public static D.gtk_window_unmaximize GtkWindowUnmaximize; public static D.gtk_window_close GtkWindowClose; + public static D.gtk_window_set_keep_above GtkWindowSetKeepAbove; public static D.gdk_window_begin_move_drag GdkWindowBeginMoveDrag; public static D.gdk_window_begin_resize_drag GdkWindowBeginResizeDrag; public static D.gdk_event_request_motions GdkEventRequestMotions; diff --git a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs index bae34db6f3..4756d33e0f 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs @@ -81,7 +81,9 @@ namespace Avalonia.Gtk3 public void ShowTaskbarIcon(bool value) => Native.GtkWindowSetSkipTaskbarHint(GtkWidget, !value); public void CanResize(bool value) => Native.GtkWindowSetResizable(GtkWidget, value); - + + public void SetTopmost(bool value) => Native.GtkWindowSetKeepAbove(GtkWidget, value); + class EmptyDisposable : IDisposable { diff --git a/src/OSX/Avalonia.MonoMac/WindowImpl.cs b/src/OSX/Avalonia.MonoMac/WindowImpl.cs index a2f8df6791..34a0702f8c 100644 --- a/src/OSX/Avalonia.MonoMac/WindowImpl.cs +++ b/src/OSX/Avalonia.MonoMac/WindowImpl.cs @@ -116,6 +116,11 @@ namespace Avalonia.MonoMac UpdateStyle(); } + public void SetTopmost(bool value) + { + Window.Level = value ? NSWindowLevel.Floating : NSWindowLevel.Normal; + } + public void SetTitle(string title) => Window.Title = title; class ModalDisposable : IDisposable diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 86dcec410b..daaee9636e 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -78,6 +78,14 @@ namespace Avalonia.Win32.Interop SWP_RESIZE = SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOZORDER } + public static class WindowPosZOrder + { + public static readonly IntPtr HWND_BOTTOM = new IntPtr(1); + public static readonly IntPtr HWND_TOP = new IntPtr(0); + public static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); + public static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); + } + public enum SizeCommand { Restored, diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 7b9f8ee066..8c0af62c74 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -32,6 +32,7 @@ namespace Avalonia.Win32 private bool _trackingMouse; private bool _decorated = true; private bool _resizable = true; + private bool _topmost = false; private double _scaling = 1; private WindowState _showWindowState; private WindowState _lastWindowState; @@ -901,5 +902,21 @@ namespace Avalonia.Win32 _resizable = value; } + + public void SetTopmost(bool value) + { + if (value == _topmost) + { + return; + } + + IntPtr hWndInsertAfter = value ? WindowPosZOrder.HWND_TOPMOST : WindowPosZOrder.HWND_NOTOPMOST; + UnmanagedMethods.SetWindowPos(_hwnd, + hWndInsertAfter, + 0, 0, 0, 0, + SetWindowPosFlags.SWP_NOMOVE | SetWindowPosFlags.SWP_NOSIZE | SetWindowPosFlags.SWP_NOACTIVATE); + + _topmost = value; + } } } From c19971a44f49e0b2aa4d52b166d69aac956422c7 Mon Sep 17 00:00:00 2001 From: CommonGuy Date: Mon, 4 Jun 2018 19:58:53 +0200 Subject: [PATCH 021/149] Replace relevant WindowPos magic number with variable --- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 8c0af62c74..97e5aa99ec 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -839,7 +839,7 @@ namespace Avalonia.Win32 var cx = Math.Abs(monitorInfo.rcWork.right - x); var cy = Math.Abs(monitorInfo.rcWork.bottom - y); - SetWindowPos(_hwnd, new IntPtr(-2), x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); + SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); } } } From 5f67bf13be951b8e42f3aa65243e55bf035b1bc7 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 5 Jun 2018 16:18:24 -0500 Subject: [PATCH 022/149] Parallelize build on Windows. --- build.cake | 1 + 1 file changed, 1 insertion(+) diff --git a/build.cake b/build.cake index 1c796e8594..df0e1ed54e 100644 --- a/build.cake +++ b/build.cake @@ -149,6 +149,7 @@ Task("Build") settings.UseToolVersion(MSBuildToolVersion.VS2017); settings.WithProperty("Windows", "True"); settings.SetNodeReuse(false); + settings.SetMaxCpuCount(0); }); } else From 2f1c94ef30358ac4a7f83a8d5616d490aeee189e Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 5 Jun 2018 18:15:23 -0500 Subject: [PATCH 023/149] Update to Cake 0.28 --- build.cake | 164 ++++++++++++++++++++++-------------------- parameters.cake | 2 - tools/packages.config | 2 +- 3 files changed, 89 insertions(+), 79 deletions(-) diff --git a/build.cake b/build.cake index df0e1ed54e..efdd62ea32 100644 --- a/build.cake +++ b/build.cake @@ -34,20 +34,31 @@ using NuGet; // PARAMETERS ////////////////////////////////////////////////////////////////////// -Parameters parameters = new Parameters(Context); -Packages packages = new Packages(Context, parameters); +class AvaloniaBuildData +{ + public AvaloniaBuildData(Parameters parameters, Packages packages) + { + Parameters = parameters; + Packages = packages; + } + + public Parameters Parameters { get; } + public Packages Packages { get; } +} /////////////////////////////////////////////////////////////////////////////// // SETUP /////////////////////////////////////////////////////////////////////////////// -Setup(context => +Setup(context => { - Information("Building version {0} of Avalonia ({1}, {2}, {3}) using version {4} of Cake.", + var parameters = new Parameters(context); + var buildContext = new AvaloniaBuildData(parameters, new Packages(context, parameters)); + + Information("Building version {0} of Avalonia ({1}, {2}) using version {3} of Cake.", parameters.Version, parameters.Platform, parameters.Configuration, - parameters.Target, typeof(ICakeContext).Assembly.GetName().Version.ToString()); if (parameters.IsRunningOnAppVeyor) @@ -55,8 +66,7 @@ Setup(context => Information("Repository Name: " + BuildSystem.AppVeyor.Environment.Repository.Name); Information("Repository Branch: " + BuildSystem.AppVeyor.Environment.Repository.Branch); } - - Information("Target: " + parameters.Target); + Information("Target:" + context.TargetTask.Name); Information("Platform: " + parameters.Platform); Information("Configuration: " + parameters.Configuration); Information("IsLocalBuild: " + parameters.IsLocalBuild); @@ -70,13 +80,15 @@ Setup(context => Information("IsReleasable: " + parameters.IsReleasable); Information("IsMyGetRelease: " + parameters.IsMyGetRelease); Information("IsNuGetRelease: " + parameters.IsNuGetRelease); + + return buildContext; }); /////////////////////////////////////////////////////////////////////////////// // TEARDOWN /////////////////////////////////////////////////////////////////////////////// -Teardown(context => +Teardown((context, buildContext) => { Information("Finished running tasks."); }); @@ -86,19 +98,19 @@ Teardown(context => /////////////////////////////////////////////////////////////////////////////// Task("Clean") - .Does(() => + .Does(data => { - CleanDirectories(parameters.BuildDirs); - CleanDirectory(parameters.ArtifactsDir); - CleanDirectory(parameters.NugetRoot); - CleanDirectory(parameters.ZipRoot); - CleanDirectory(parameters.BinRoot); + CleanDirectories(data.Parameters.BuildDirs); + CleanDirectory(data.Parameters.ArtifactsDir); + CleanDirectory(data.Parameters.NugetRoot); + CleanDirectory(data.Parameters.ZipRoot); + CleanDirectory(data.Parameters.BinRoot); }); Task("Restore-NuGet-Packages") .IsDependentOn("Clean") - .WithCriteria(parameters.IsRunningOnWindows) - .Does(() => + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) + .Does(data => { var maxRetryCount = 5; var toolTimeout = 2d; @@ -115,13 +127,13 @@ Task("Restore-NuGet-Packages") toolTimeout+=0.5; }}) .Execute(()=> { - NuGetRestore(parameters.MSBuildSolution, new NuGetRestoreSettings { + NuGetRestore(data.Parameters.MSBuildSolution, new NuGetRestoreSettings { ToolTimeout = TimeSpan.FromMinutes(toolTimeout) }); }); }); -void DotNetCoreBuild() +void DotNetCoreBuild(Parameters parameters) { var settings = new DotNetCoreBuildSettings { @@ -137,14 +149,14 @@ void DotNetCoreBuild() Task("Build") .IsDependentOn("Restore-NuGet-Packages") - .Does(() => + .Does(data => { - if(parameters.IsRunningOnWindows) + if(data.Parameters.IsRunningOnWindows) { - MSBuild(parameters.MSBuildSolution, settings => { - settings.SetConfiguration(parameters.Configuration); + MSBuild(data.Parameters.MSBuildSolution, settings => { + settings.SetConfiguration(data.Parameters.Configuration); settings.SetVerbosity(Verbosity.Minimal); - settings.WithProperty("Platform", "\"" + parameters.Platform + "\""); + settings.WithProperty("Platform", "\"" + data.Parameters.Platform + "\""); settings.WithProperty("UseRoslynPathHack", "true"); settings.UseToolVersion(MSBuildToolVersion.VS2017); settings.WithProperty("Windows", "True"); @@ -154,7 +166,7 @@ Task("Build") } else { - DotNetCoreBuild(); + DotNetCoreBuild(data.Parameters); } }); @@ -185,66 +197,66 @@ Task("Run-Unit-Tests") .IsDependentOn("Build") .IsDependentOn("Run-Designer-Tests") .IsDependentOn("Run-Render-Tests") - .WithCriteria(() => !parameters.SkipTests) - .Does(() => { - RunCoreTest("./tests/Avalonia.Base.UnitTests", parameters, false); - RunCoreTest("./tests/Avalonia.Controls.UnitTests", parameters, false); - RunCoreTest("./tests/Avalonia.Input.UnitTests", parameters, false); - RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", parameters, false); - RunCoreTest("./tests/Avalonia.Layout.UnitTests", parameters, false); - RunCoreTest("./tests/Avalonia.Markup.UnitTests", parameters, false); - RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", parameters, false); - RunCoreTest("./tests/Avalonia.Styling.UnitTests", parameters, false); - RunCoreTest("./tests/Avalonia.Visuals.UnitTests", parameters, false); - if (parameters.IsRunningOnWindows) + .WithCriteria((context, data) => !data.Parameters.SkipTests) + .Does(data => { + RunCoreTest("./tests/Avalonia.Base.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Controls.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Input.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Layout.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Markup.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Styling.UnitTests", data.Parameters, false); + RunCoreTest("./tests/Avalonia.Visuals.UnitTests", data.Parameters, false); + if (data.Parameters.IsRunningOnWindows) { - RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", parameters, true); + RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", data.Parameters, true); } }); Task("Run-Designer-Tests") .IsDependentOn("Build") - .WithCriteria(() => !parameters.SkipTests) - .Does(() => { - RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", parameters, false); + .WithCriteria((context, data) => !data.Parameters.SkipTests) + .Does(data => { + RunCoreTest("./tests/Avalonia.DesignerSupport.Tests", data.Parameters, false); }); Task("Run-Render-Tests") .IsDependentOn("Build") - .WithCriteria(() => !parameters.SkipTests && parameters.IsRunningOnWindows) - .Does(() => { - RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", parameters, true); - RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", parameters, true); + .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) + .Does(data => { + RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj", data.Parameters, true); + RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", data.Parameters, true); }); Task("Copy-Files") .IsDependentOn("Run-Unit-Tests") - .Does(() => + .Does(data => { - CopyFiles(packages.BinFiles, parameters.BinRoot); + CopyFiles(data.Packages.BinFiles, data.Parameters.BinRoot); }); Task("Zip-Files") .IsDependentOn("Copy-Files") - .Does(() => + .Does(data => { - Zip(parameters.BinRoot, parameters.ZipCoreArtifacts); - - Zip(parameters.ZipSourceControlCatalogDesktopDirs, - parameters.ZipTargetControlCatalogDesktopDirs, - GetFiles(parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dll") + - GetFiles(parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.config") + - GetFiles(parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.so") + - GetFiles(parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dylib") + - GetFiles(parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.exe")); + Zip(data.Parameters.BinRoot, data.Parameters.ZipCoreArtifacts); + + Zip(data.Parameters.ZipSourceControlCatalogDesktopDirs, + data.Parameters.ZipTargetControlCatalogDesktopDirs, + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dll") + + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.config") + + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.so") + + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.dylib") + + GetFiles(data.Parameters.ZipSourceControlCatalogDesktopDirs.FullPath + "/*.exe")); }); Task("Create-NuGet-Packages") .IsDependentOn("Run-Unit-Tests") .IsDependentOn("Inspect") - .Does(() => + .Does(data => { - foreach(var nuspec in packages.NuspecNuGetSettings) + foreach(var nuspec in data.Packages.NuspecNuGetSettings) { NuGetPack(nuspec); } @@ -252,12 +264,12 @@ Task("Create-NuGet-Packages") Task("Publish-MyGet") .IsDependentOn("Create-NuGet-Packages") - .WithCriteria(() => !parameters.IsLocalBuild) - .WithCriteria(() => !parameters.IsPullRequest) - .WithCriteria(() => parameters.IsMainRepo) - .WithCriteria(() => parameters.IsMasterBranch) - .WithCriteria(() => parameters.IsMyGetRelease) - .Does(() => + .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) + .WithCriteria((context, data) => !data.Parameters.IsPullRequest) + .WithCriteria((context, data) => data.Parameters.IsMainRepo) + .WithCriteria((context, data) => data.Parameters.IsMasterBranch) + .WithCriteria((context, data) => data.Parameters.IsMyGetRelease) + .Does(data => { var apiKey = EnvironmentVariable("MYGET_API_KEY"); if(string.IsNullOrEmpty(apiKey)) @@ -271,7 +283,7 @@ Task("Publish-MyGet") throw new InvalidOperationException("Could not resolve MyGet API url."); } - foreach(var nupkg in packages.NugetPackages) + foreach(var nupkg in data.Packages.NugetPackages) { NuGetPush(nupkg, new NuGetPushSettings { Source = apiUrl, @@ -286,11 +298,11 @@ Task("Publish-MyGet") Task("Publish-NuGet") .IsDependentOn("Create-NuGet-Packages") - .WithCriteria(() => !parameters.IsLocalBuild) - .WithCriteria(() => !parameters.IsPullRequest) - .WithCriteria(() => parameters.IsMainRepo) - .WithCriteria(() => parameters.IsNuGetRelease) - .Does(() => + .WithCriteria((context, data) => !data.Parameters.IsLocalBuild) + .WithCriteria((context, data) => !data.Parameters.IsPullRequest) + .WithCriteria((context, data) => data.Parameters.IsMainRepo) + .WithCriteria((context, data) => data.Parameters.IsNuGetRelease) + .Does(data => { var apiKey = EnvironmentVariable("NUGET_API_KEY"); if(string.IsNullOrEmpty(apiKey)) @@ -304,7 +316,7 @@ Task("Publish-NuGet") throw new InvalidOperationException("Could not resolve NuGet API url."); } - foreach(var nupkg in packages.NugetPackages) + foreach(var nupkg in data.Packages.NugetPackages) { NuGetPush(nupkg, new NuGetPushSettings { ApiKey = apiKey, @@ -318,7 +330,7 @@ Task("Publish-NuGet") }); Task("Run-Leak-Tests") - .WithCriteria(parameters.IsRunningOnWindows) + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) .IsDependentOn("Build") .Does(() => { @@ -358,7 +370,7 @@ Task("Run-Leak-Tests") }); Task("Inspect") - .WithCriteria(parameters.IsRunningOnWindows) + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) .IsDependentOn("Restore-NuGet-Packages") .Does(() => { @@ -396,9 +408,9 @@ Task("Inspect") Task("Package") .IsDependentOn("Create-NuGet-Packages"); -Task("Default").Does(() => +Task("Default").Does(data => { - if(parameters.IsRunningOnWindows) + if(data.Parameters.IsRunningOnWindows) RunTarget("Package"); else RunTarget("Run-Unit-Tests"); @@ -415,4 +427,4 @@ Task("Travis") // EXECUTE /////////////////////////////////////////////////////////////////////////////// -RunTarget(parameters.Target); +RunTarget(Context.Argument("target", "Default")); diff --git a/parameters.cake b/parameters.cake index 759846c658..ffd472cbd4 100644 --- a/parameters.cake +++ b/parameters.cake @@ -1,6 +1,5 @@ public class Parameters { - public string Target { get; private set; } public string Platform { get; private set; } public string Configuration { get; private set; } public bool SkipTests { get; private set; } @@ -43,7 +42,6 @@ public class Parameters var buildSystem = context.BuildSystem(); // ARGUMENTS - Target = context.Argument("target", "Default"); Platform = context.Argument("platform", "Any CPU"); Configuration = context.Argument("configuration", "Release"); SkipTests = context.HasArgument("skip-tests"); diff --git a/tools/packages.config b/tools/packages.config index e52a2c7e98..3c65df896f 100644 --- a/tools/packages.config +++ b/tools/packages.config @@ -1,4 +1,4 @@ - + From 8b36caf88e731b81f7522b6d1c9971f9a1c404ea Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 5 Jun 2018 18:15:57 -0500 Subject: [PATCH 024/149] Disable 'Unused' warning in Gtk/Interop/Native.cs --- src/Gtk/Avalonia.Gtk3/Interop/Native.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs index 1adaf9f4e1..b018f4db3f 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -1,4 +1,5 @@ -using System; +#pragma warning disable 649 +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; From f06fa9881948fc1ea5a87e2ebb28ee6605d95d72 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jun 2018 00:49:38 +0100 Subject: [PATCH 025/149] fix exception when carousel items set to null. --- src/Avalonia.Controls/Presenters/CarouselPresenter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 144174e371..35deea05ec 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -125,8 +125,8 @@ namespace Avalonia.Controls.Presenters var containers = generator.Containers.ToList(); generator.Clear(); Panel.Children.RemoveAll(containers.Select(x => x.ContainerControl)); - - MoveToPage(-1, SelectedIndex >= 0 ? SelectedIndex : 0); + + MoveToPage(-1, SelectedIndex >= 0 ? SelectedIndex : e.NewStartingIndex); } break; } From 4169b8de0cbd6bc7d572d552ac66bbe5fa1a7701 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 6 Jun 2018 00:12:55 -0500 Subject: [PATCH 026/149] Clean up assembly version conflicts when building tests. --- Avalonia.sln | 5 ++--- build/XUnit.props | 17 +---------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index 9cf93e8a84..54f6f5e7e7 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -105,9 +105,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Desktop", "s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{57E0455D-D565-44BB-B069-EE1AA20F8337}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{52F55355-D120-42AC-8116-8410A7D602FA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{52F55355-D120-42AC-8116-8410A7D602FA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}" EndProject @@ -141,7 +141,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props build\Moq.props = build\Moq.props build\NetCore.props = build\NetCore.props - build\NetFX.props = build\NetFX.props build\ReactiveUI.props = build\ReactiveUI.props build\Rx.props = build\Rx.props build\SampleApp.props = build\SampleApp.props diff --git a/build/XUnit.props b/build/XUnit.props index 15412d19e7..079565d184 100644 --- a/build/XUnit.props +++ b/build/XUnit.props @@ -9,21 +9,6 @@ + - - - - - true - - - - - true - - - From df593b3a8084a47e94cb94221485a5f3823f1a1c Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 6 Jun 2018 00:14:15 -0500 Subject: [PATCH 027/149] Get leak tests running again in the build script. They were removed at some point. Also, update the tooling used and clean up the code for running them. --- build.cake | 81 ++++++++----------- cake.config | 15 ++++ .../Avalonia.LeakTests.csproj | 8 +- .../Properties/AssemblyInfo.cs | 8 ++ .../toolproject/tool.csproj | 11 --- .../Avalonia.UnitTests.csproj | 4 +- 6 files changed, 61 insertions(+), 66 deletions(-) create mode 100644 cake.config create mode 100644 tests/Avalonia.LeakTests/Properties/AssemblyInfo.cs delete mode 100644 tests/Avalonia.LeakTests/toolproject/tool.csproj diff --git a/build.cake b/build.cake index efdd62ea32..068b388293 100644 --- a/build.cake +++ b/build.cake @@ -10,7 +10,8 @@ // TOOLS /////////////////////////////////////////////////////////////////////////////// -#tool "nuget:?package=xunit.runner.console&version=2.3.0-beta5-build3769" +#tool "nuget:?package=xunit.runner.console&version=2.3.1" +#tool "nuget:?package=JetBrains.dotMemoryUnit&version=3.0.20171219.105559" /////////////////////////////////////////////////////////////////////////////// // USINGS @@ -195,8 +196,6 @@ void RunCoreTest(string project, Parameters parameters, bool coreOnly = false) Task("Run-Unit-Tests") .IsDependentOn("Build") - .IsDependentOn("Run-Designer-Tests") - .IsDependentOn("Run-Render-Tests") .WithCriteria((context, data) => !data.Parameters.SkipTests) .Does(data => { RunCoreTest("./tests/Avalonia.Base.UnitTests", data.Parameters, false); @@ -229,8 +228,36 @@ Task("Run-Render-Tests") RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", data.Parameters, true); }); -Task("Copy-Files") +Task("Run-Leak-Tests") + .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) + .IsDependentOn("Build") + .Does(() => + { + var dotMemoryUnit = Context.Tools.Resolve("dotMemoryUnit.exe"); + var leakTestsExitCode = StartProcess(dotMemoryUnit, new ProcessSettings + { + Arguments = new ProcessArgumentBuilder() + .Append(Context.Tools.Resolve("xunit.console.x86.exe").FullPath) + .Append("--propagate-exit-code") + .Append("--") + .Append("tests\\Avalonia.LeakTests\\bin\\Release\\net47\\Avalonia.LeakTests.dll"), + Timeout = 120000 + }); + + if (leakTestsExitCode != 0) + { + throw new Exception("Leak Tests failed"); + } + }); + +Task("Run-Tests") .IsDependentOn("Run-Unit-Tests") + .IsDependentOn("Run-Render-Tests") + .IsDependentOn("Run-Designer-Tests") + .IsDependentOn("Run-Leak-Tests"); + +Task("Copy-Files") + .IsDependentOn("Run-Tests") .Does(data => { CopyFiles(data.Packages.BinFiles, data.Parameters.BinRoot); @@ -252,7 +279,7 @@ Task("Zip-Files") }); Task("Create-NuGet-Packages") - .IsDependentOn("Run-Unit-Tests") + .IsDependentOn("Run-Tests") .IsDependentOn("Inspect") .Does(data => { @@ -329,46 +356,6 @@ Task("Publish-NuGet") Information("Publish-NuGet Task failed, but continuing with next Task..."); }); -Task("Run-Leak-Tests") - .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) - .IsDependentOn("Build") - .Does(() => - { - DotNetCoreRestore("tests\\Avalonia.LeakTests\\toolproject\\tool.csproj"); - DotNetBuild("tests\\Avalonia.LeakTests\\toolproject\\tool.csproj", settings => settings.SetConfiguration("Release")); - var report = "tests\\Avalonia.LeakTests\\bin\\Release\\report.xml"; - if(System.IO.File.Exists(report)) - System.IO.File.Delete(report); - - var toolXunitConsoleX86 = Context.Tools.Resolve("xunit.console.x86.exe").FullPath; - var proc = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName="tests\\Avalonia.LeakTests\\toolproject\\bin\\dotMemoryUnit.exe", - Arguments="-targetExecutable=\"" + toolXunitConsoleX86 + "\" -returnTargetExitCode -- tests\\Avalonia.LeakTests\\bin\\Release\\Avalonia.LeakTests.dll -xml tests\\Avalonia.LeakTests\\bin\\Release\\report.xml ", - UseShellExecute = false, - }); - var st = System.Diagnostics.Stopwatch.StartNew(); - while(!proc.HasExited && !System.IO.File.Exists(report)) - { - if(st.Elapsed.TotalSeconds>60) - { - Error("Timed out, probably a bug in dotMemoryUnit"); - proc.Kill(); - throw new Exception("dotMemory issue"); - } - proc.WaitForExit(100); - } - try{ - proc.Kill(); - }catch{} - var doc = System.Xml.Linq.XDocument.Load(report); - if(doc.Root.Descendants("assembly").Any(x=>x.Attribute("failed").Value.ToString() != "0")) - { - throw new Exception("Tests failed"); - } - - }); - Task("Inspect") .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) .IsDependentOn("Restore-NuGet-Packages") @@ -413,7 +400,7 @@ Task("Default").Does(data => if(data.Parameters.IsRunningOnWindows) RunTarget("Package"); else - RunTarget("Run-Unit-Tests"); + RunTarget("Run-Tests"); }); Task("AppVeyor") .IsDependentOn("Zip-Files") @@ -421,7 +408,7 @@ Task("AppVeyor") .IsDependentOn("Publish-NuGet"); Task("Travis") - .IsDependentOn("Run-Unit-Tests"); + .IsDependentOn("Run-Tests"); /////////////////////////////////////////////////////////////////////////////// // EXECUTE diff --git a/cake.config b/cake.config new file mode 100644 index 0000000000..8089cd4084 --- /dev/null +++ b/cake.config @@ -0,0 +1,15 @@ +; This is the default configuration file for Cake. +; This file was downloaded from https://github.com/cake-build/resources + +[Nuget] +Source=https://api.nuget.org/v3/index.json +UseInProcessClient=true +LoadDependencies=false + +[Paths] +Tools=./tools +Addins=./tools/Addins +Modules=./tools/Modules + +[Settings] +SkipVerification=false diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index c945db8085..7966cac845 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -1,13 +1,12 @@  - netcoreapp2.0 + net47 - @@ -19,13 +18,10 @@ - + - - - \ No newline at end of file diff --git a/tests/Avalonia.LeakTests/Properties/AssemblyInfo.cs b/tests/Avalonia.LeakTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..4cc100aa28 --- /dev/null +++ b/tests/Avalonia.LeakTests/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// 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.Reflection; +using Xunit; + +// Don't run tests in parallel. +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/tests/Avalonia.LeakTests/toolproject/tool.csproj b/tests/Avalonia.LeakTests/toolproject/tool.csproj deleted file mode 100644 index 54dbe6f17e..0000000000 --- a/tests/Avalonia.LeakTests/toolproject/tool.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - $(MSBuildThisFileDirectory)\bin - $(OutputPath) - net461 - Library - - - - - \ No newline at end of file diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index 6fe61b17f7..d86b27e804 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -1,6 +1,6 @@  - netcoreapp2.0 + netcoreapp2.0;net461 false Library @@ -16,7 +16,7 @@ - + From 93774af3a445af29a359ac330abb908cee02458e Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 6 Jun 2018 00:14:35 -0500 Subject: [PATCH 028/149] Update Android resource files. --- .../Resources/Resource.Designer.cs | 9 ++------- .../Avalonia.Android/Resources/Resource.Designer.cs | 7 ++----- .../Resources/Resource.Designer.cs | 9 ++------- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs index cee3331ba8..96f0e76fd8 100644 --- a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs +++ b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs @@ -28,8 +28,6 @@ namespace ControlCatalog.Android { global::Avalonia.Android.Resource.String.ApplicationName = global::ControlCatalog.Android.Resource.String.ApplicationName; global::Avalonia.Android.Resource.String.Hello = global::ControlCatalog.Android.Resource.String.Hello; - global::Avalonia.Android.Resource.String.library_name = global::ControlCatalog.Android.Resource.String.library_name; - global::Splat.Resource.String.library_name = global::ControlCatalog.Android.Resource.String.library_name; } public partial class Attribute @@ -96,14 +94,11 @@ namespace ControlCatalog.Android public partial class String { - // aapt resource value: 0x7f040002 - public const int ApplicationName = 2130968578; - // aapt resource value: 0x7f040001 - public const int Hello = 2130968577; + public const int ApplicationName = 2130968577; // aapt resource value: 0x7f040000 - public const int library_name = 2130968576; + public const int Hello = 2130968576; static String() { diff --git a/src/Android/Avalonia.Android/Resources/Resource.Designer.cs b/src/Android/Avalonia.Android/Resources/Resource.Designer.cs index e66c2800d3..80cbbc51ec 100644 --- a/src/Android/Avalonia.Android/Resources/Resource.Designer.cs +++ b/src/Android/Avalonia.Android/Resources/Resource.Designer.cs @@ -40,14 +40,11 @@ namespace Avalonia.Android public partial class String { - // aapt resource value: 0x7f020002 - public static int ApplicationName = 2130837506; - // aapt resource value: 0x7f020001 - public static int Hello = 2130837505; + public static int ApplicationName = 2130837505; // aapt resource value: 0x7f020000 - public static int library_name = 2130837504; + public static int Hello = 2130837504; static String() { diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs index 91327cf941..e171dd6162 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs +++ b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs @@ -28,8 +28,6 @@ namespace Avalonia.AndroidTestApplication { global::Avalonia.Android.Resource.String.ApplicationName = global::Avalonia.AndroidTestApplication.Resource.String.ApplicationName; global::Avalonia.Android.Resource.String.Hello = global::Avalonia.AndroidTestApplication.Resource.String.Hello; - global::Avalonia.Android.Resource.String.library_name = global::Avalonia.AndroidTestApplication.Resource.String.library_name; - global::Splat.Resource.String.library_name = global::Avalonia.AndroidTestApplication.Resource.String.library_name; } public partial class Attribute @@ -64,14 +62,11 @@ namespace Avalonia.AndroidTestApplication public partial class String { - // aapt resource value: 0x7f030002 - public const int ApplicationName = 2130903042; - // aapt resource value: 0x7f030001 - public const int Hello = 2130903041; + public const int ApplicationName = 2130903041; // aapt resource value: 0x7f030000 - public const int library_name = 2130903040; + public const int Hello = 2130903040; static String() { From 046322e6b04242e69ef9fdbe7f0b9938117dfd2d Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 6 Jun 2018 00:39:38 -0500 Subject: [PATCH 029/149] Skip all tests when -skip-tests passed to build script. --- build.cake | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/build.cake b/build.cake index 068b388293..a64fbf0fba 100644 --- a/build.cake +++ b/build.cake @@ -229,7 +229,7 @@ Task("Run-Render-Tests") }); Task("Run-Leak-Tests") - .WithCriteria((context, data) => data.Parameters.IsRunningOnWindows) + .WithCriteria((context, data) => !data.Parameters.SkipTests && data.Parameters.IsRunningOnWindows) .IsDependentOn("Build") .Does(() => { @@ -395,13 +395,6 @@ Task("Inspect") Task("Package") .IsDependentOn("Create-NuGet-Packages"); -Task("Default").Does(data => -{ - if(data.Parameters.IsRunningOnWindows) - RunTarget("Package"); - else - RunTarget("Run-Tests"); -}); Task("AppVeyor") .IsDependentOn("Zip-Files") .IsDependentOn("Publish-MyGet") @@ -414,4 +407,11 @@ Task("Travis") // EXECUTE /////////////////////////////////////////////////////////////////////////////// -RunTarget(Context.Argument("target", "Default")); +var target = Context.Argument("target", "Default"); + +if (target == "Default") +{ + target = Context.IsRunningOnWindows() ? "Package" : "Run-Tests"; +} + +RunTarget(target); From 18f436a2c3e1ba70b573c5a1aade5746431cce8a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 6 Jun 2018 01:34:24 -0500 Subject: [PATCH 030/149] Clean up unused code warnings and malformed doc comments. --- .../interop/Direct3DInteropSample/MainWindow.cs | 1 - .../Specific/Helpers/AndroidTouchEventsHelper.cs | 2 -- src/Avalonia.Animation/AnimatorKeyFrame.cs | 2 +- .../Presenters/CarouselPresenter.cs | 4 ++++ src/Avalonia.Remote.Protocol/TcpTransportBase.cs | 4 +--- src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs | 6 +++--- src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs | 5 +---- .../FramebufferToplevelImpl.cs | 6 +++++- .../PortableXaml/TypeDescriptorExtensions.cs | 14 +++++++------- src/OSX/Avalonia.MonoMac/ClipboardImpl.cs | 3 ++- .../Media/Imaging/D2DBitmapImpl.cs | 1 + .../Media/TransformedGeometryImpl.cs | 1 + .../Avalonia.Win32/Interop/UnmanagedMethods.cs | 4 ++-- src/iOS/Avalonia.iOS/DisplayLinkRenderLoop.cs | 2 +- src/iOS/Avalonia.iOS/TopLevelImpl.cs | 3 +-- .../AvaloniaObjectTests_Threading.cs | 2 ++ .../InteractiveTests.cs | 2 +- tests/Avalonia.RenderTests/TestBase.cs | 2 ++ 18 files changed, 35 insertions(+), 29 deletions(-) diff --git a/samples/interop/Direct3DInteropSample/MainWindow.cs b/samples/interop/Direct3DInteropSample/MainWindow.cs index ffa0de0a36..19c31a3af1 100644 --- a/samples/interop/Direct3DInteropSample/MainWindow.cs +++ b/samples/interop/Direct3DInteropSample/MainWindow.cs @@ -88,7 +88,6 @@ namespace Direct3DInteropSample context.ClearDepthStencilView(depthView, DepthStencilClearFlags.Depth, 1.0f, 0); context.ClearRenderTargetView(renderView, Color.White); - var time = 50; // Update WorldViewProj Matrix var worldViewProj = Matrix.RotationX((float) _model.RotationX) * Matrix.RotationY((float) _model.RotationY) * Matrix.RotationZ((float) _model.RotationZ) diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs index 0f90472bd0..71822a6f47 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs @@ -126,8 +126,6 @@ namespace Avalonia.Android.Platform.Specific.Helpers return e.Action != MotionEventActions.Up; } - private Paint _paint; - public void Dispose() { HandleEvents = false; diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index 875bf761c4..02457cb9aa 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -9,7 +9,7 @@ namespace Avalonia.Animation { /// /// Defines a KeyFrame that is used for - /// objects. + /// objects. /// public class AnimatorKeyFrame { diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 144174e371..0f9bfe4de0 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -115,7 +115,9 @@ namespace Avalonia.Controls.Presenters var containers = generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count); Panel.Children.RemoveAll(containers.Select(x => x.ContainerControl)); +#pragma warning disable 4014 MoveToPage(-1, SelectedIndex); +#pragma warning restore 4014 } break; @@ -126,7 +128,9 @@ namespace Avalonia.Controls.Presenters generator.Clear(); Panel.Children.RemoveAll(containers.Select(x => x.ContainerControl)); +#pragma warning disable 4014 MoveToPage(-1, SelectedIndex >= 0 ? SelectedIndex : 0); +#pragma warning restore 4014 } break; } diff --git a/src/Avalonia.Remote.Protocol/TcpTransportBase.cs b/src/Avalonia.Remote.Protocol/TcpTransportBase.cs index 3d2bd09485..562dbdf8f9 100644 --- a/src/Avalonia.Remote.Protocol/TcpTransportBase.cs +++ b/src/Avalonia.Remote.Protocol/TcpTransportBase.cs @@ -48,14 +48,12 @@ namespace Avalonia.Remote.Protocol { var cl = await server.AcceptTcpClientAsync(); AcceptNew(); - Task.Run(async () => + await Task.Run(async () => { var tcs = new TaskCompletionSource(); var t = CreateTransport(_resolver, cl.GetStream(), () => tcs.TrySetResult(0)); cb(t); await tcs.Task; - - }); } catch diff --git a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs index c2005c9acb..cb996867c4 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontFamilyKey.cs @@ -7,7 +7,7 @@ using System.Linq; namespace Avalonia.Media.Fonts { /// - /// Represents an identifier for a + /// Represents an identifier for a /// public class FontFamilyKey { @@ -33,12 +33,12 @@ namespace Avalonia.Media.Fonts } /// - /// Location of stored font asset that belongs to a + /// Location of stored font asset that belongs to a /// public Uri Location { get; } /// - /// Optional filename for a font asset that belongs to a + /// Optional filename for a font asset that belongs to a /// public string FileName { get; } diff --git a/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs b/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs index 0ab4ef980c..971edb1364 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs @@ -21,20 +21,17 @@ namespace Avalonia.Gtk3.Interop } return true; } - - private static readonly GCHandle PinnedHandle; + private static readonly Native.D.timeout_callback PinnedHandler; static GlibTimeout() { PinnedHandler = Handler; - } public static void Add(int priority, uint interval, Func callback) { var handle = GCHandle.Alloc(callback); - //Native.GTimeoutAdd(interval, PinnedHandler, GCHandle.ToIntPtr(handle)); Native.GTimeoutAddFull(priority, interval, PinnedHandler, GCHandle.ToIntPtr(handle), IntPtr.Zero); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 0db622ba13..627b508aa8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -70,6 +70,10 @@ namespace Avalonia.LinuxFramebuffer public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action Closed { get; set; } - public event Action LostFocus; + public event Action LostFocus + { + add {} + remove {} + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/TypeDescriptorExtensions.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/TypeDescriptorExtensions.cs index 81525fef7d..458b09b66e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/TypeDescriptorExtensions.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/TypeDescriptorExtensions.cs @@ -14,13 +14,13 @@ namespace Portable.Xaml.ComponentModel /// Gets the service from ITypeDescriptorContext /// usually in TypeConverter in xaml reader context /// examples: - /// context.GetService() - /// context.GetService() - /// context.GetService() - /// context.GetService() - /// context.GetService() - /// context.GetService() - /// context.GetService() + /// context.GetService<IXamlTypeResolver>() + /// context.GetService<IXamlNamespaceResolver>() + /// context.GetService<IXamlNameProvider>() + /// context.GetService<INamespacePrefixLookup>() + /// context.GetService<IXamlSchemaContextProvider>() + /// context.GetService<IRootObjectProvider>() + /// context.GetService<IProvideValueTarget>() /// /// Service Type /// The TypeDescriptor context. diff --git a/src/OSX/Avalonia.MonoMac/ClipboardImpl.cs b/src/OSX/Avalonia.MonoMac/ClipboardImpl.cs index f7b98c0c1f..29eb3720ec 100644 --- a/src/OSX/Avalonia.MonoMac/ClipboardImpl.cs +++ b/src/OSX/Avalonia.MonoMac/ClipboardImpl.cs @@ -22,9 +22,10 @@ namespace Avalonia.MonoMac return Task.CompletedTask; } - public async Task ClearAsync() + public Task ClearAsync() { NSPasteboard.GeneralPasteboard.ClearContents(); + return Task.CompletedTask; } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs index b03e022674..6713cb13be 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs @@ -19,6 +19,7 @@ namespace Avalonia.Direct2D1.Media /// Initialize a new instance of the class /// with a bitmap backed by GPU memory. /// + /// The image factory to use when saving out this bitmap. /// The GPU bitmap. /// /// This bitmap must be either from the same render target, diff --git a/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs index e0e9e340bb..c00a826d0c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs @@ -11,6 +11,7 @@ namespace Avalonia.Direct2D1.Media /// /// Initializes a new instance of the class. /// + /// The source geometry. /// An existing Direct2D . public TransformedGeometryImpl(TransformedGeometry geometry, GeometryImpl source) : base(geometry) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 86dcec410b..4095447943 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -998,7 +998,7 @@ namespace Avalonia.Win32.Interop public static extern int DragQueryFile(IntPtr hDrop, int iFile, StringBuilder lpszFile, int cch); [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)] - public static extern void DoDragDrop(IOleDataObject dataObject, IDropSource dropSource, int allowedEffects, int[] finalEffect); + internal static extern void DoDragDrop(IOleDataObject dataObject, IDropSource dropSource, int allowedEffects, int[] finalEffect); @@ -1408,7 +1408,7 @@ namespace Avalonia.Win32.Interop [ComImport] [Guid("0000010E-0000-0000-C000-000000000046")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IOleDataObject + internal interface IOleDataObject { void GetData([In] ref FORMATETC format, out STGMEDIUM medium); void GetDataHere([In] ref FORMATETC format, ref STGMEDIUM medium); diff --git a/src/iOS/Avalonia.iOS/DisplayLinkRenderLoop.cs b/src/iOS/Avalonia.iOS/DisplayLinkRenderLoop.cs index ef045f61bf..a6d1bf1670 100644 --- a/src/iOS/Avalonia.iOS/DisplayLinkRenderLoop.cs +++ b/src/iOS/Avalonia.iOS/DisplayLinkRenderLoop.cs @@ -26,7 +26,7 @@ namespace Avalonia.iOS { Tick?.Invoke(this, new EventArgs()); } - catch (Exception e) + catch (Exception) { //TODO: log } diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs index 5d0074e6db..3a2d2a33c6 100644 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs @@ -26,7 +26,6 @@ namespace Avalonia.iOS { private IInputRoot _inputRoot; private readonly KeyboardEventsHelper _keyboardHelper; - private Point _position; public TopLevelImpl() { @@ -52,7 +51,7 @@ namespace Avalonia.iOS public Action Resized { get; set; } public Action ScalingChanged { get; set; } - public IPlatformHandle Handle => null; + public new IPlatformHandle Handle => null; public double Scaling => UIScreen.MainScreen.Scale; diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs index 78823a370b..e067886742 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs @@ -160,7 +160,9 @@ namespace Avalonia.Base.UnitTests public bool CurrentThreadIsLoopThread { get; set; } +#pragma warning disable 67 public event Action Signaled; +#pragma warning restore 67 public void RunLoop(CancellationToken cancellationToken) { diff --git a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs index 067cf85c3c..58ee63cea4 100644 --- a/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs @@ -403,7 +403,7 @@ namespace Avalonia.Interactivity.UnitTests private class TestInteractive : Interactive { public bool ClassHandlerInvoked { get; private set; } - public string Name { get; set; } + public new string Name { get; set; } public IEnumerable Children { diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 321dbc4fbe..19413b32eb 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -161,7 +161,9 @@ namespace Avalonia.Direct2D1.RenderTests public Thread MainThread { get; set; } +#pragma warning disable 67 public event Action Signaled; +#pragma warning restore 67 public void RunLoop(CancellationToken cancellationToken) { From 38de22cf7563217bbd4ce7644604e46aa77bef48 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 6 Jun 2018 01:46:32 -0500 Subject: [PATCH 031/149] Add empty event implementations for unused events. --- .../Remote/DetachableTransportConnection.cs | 6 +++++- src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs | 6 +++++- .../Remote/RemoteDesignerEntryPoint.cs | 6 +++++- src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs | 6 +++++- src/iOS/Avalonia.iOS/EmbeddableImpl.cs | 6 +++++- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.DesignerSupport/Remote/DetachableTransportConnection.cs b/src/Avalonia.DesignerSupport/Remote/DetachableTransportConnection.cs index 1fb10a3a7c..9c125448a0 100644 --- a/src/Avalonia.DesignerSupport/Remote/DetachableTransportConnection.cs +++ b/src/Avalonia.DesignerSupport/Remote/DetachableTransportConnection.cs @@ -30,6 +30,10 @@ namespace Avalonia.DesignerSupport.Remote public event Action OnMessage; - public event Action OnException; + public event Action OnException + { + add {} + remove {} + } } } \ No newline at end of file diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index ef16d06b60..61bd9670f2 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -44,7 +44,11 @@ namespace Avalonia.DesignerSupport.Remote public WindowState WindowState { get; set; } public Action WindowStateChanged { get; set; } public Size MaxClientSize { get; } = new Size(4096, 4096); - public event Action LostFocus; + public event Action LostFocus + { + add {} + remove {} + } protected override void OnMessage(IAvaloniaRemoteTransportConnection transport, object obj) { diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index f5893ae69a..2857272ac1 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -125,7 +125,11 @@ namespace Avalonia.DesignerSupport.Remote class NeverClose : ICloseable { - public event EventHandler Closed; + public event EventHandler Closed + { + add {} + remove {} + } } public static void Main(string[] cmdline) diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index fb308e62b8..3580b4fcb5 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -26,7 +26,11 @@ namespace Avalonia.Markup.Xaml.Styling } /// - public event EventHandler ResourcesChanged; + public event EventHandler ResourcesChanged + { + add {} + remove {} + } /// /// Gets or sets the source URL. diff --git a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs index 3d8bafeca9..7d34cf40f7 100644 --- a/src/iOS/Avalonia.iOS/EmbeddableImpl.cs +++ b/src/iOS/Avalonia.iOS/EmbeddableImpl.cs @@ -27,6 +27,10 @@ namespace Avalonia.iOS { } - public event Action LostFocus; + public event Action LostFocus + { + add {} + remove {} + } } } From 3c3b5b63b3b84606309195d3fe8ece1e72a051da Mon Sep 17 00:00:00 2001 From: CommonGuy Date: Wed, 6 Jun 2018 14:59:29 +0200 Subject: [PATCH 032/149] Use newer WinApi for OpenFileDialog and SaveFileDialog --- .../Interop/UnmanagedMethods.cs | 313 ++++++++++++------ .../Avalonia.Win32/SystemDialogImpl.cs | 228 +++++-------- 2 files changed, 295 insertions(+), 246 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 86dcec410b..e1048bc2e7 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1184,16 +1184,6 @@ namespace Avalonia.Win32.Interop public IntPtr hIconSm; } - [Flags] - public enum OpenFileNameFlags - { - OFN_ALLOWMULTISELECT = 0x00000200, - OFN_EXPLORER = 0x00080000, - OFN_HIDEREADONLY = 0x00000004, - OFN_NOREADONLYRETURN = 0x00008000, - OFN_OVERWRITEPROMPT = 0x00000002 - } - public enum HRESULT : uint { S_FALSE = 0x0001, @@ -1213,7 +1203,7 @@ namespace Avalonia.Win32.Interop public const uint SIGDN_FILESYSPATH = 0x80058000; [Flags] - internal enum FOS : uint + public enum FOS : uint { FOS_OVERWRITEPROMPT = 0x00000002, FOS_STRICTFILETYPES = 0x00000004, @@ -1237,135 +1227,246 @@ namespace Avalonia.Win32.Interop FOS_DEFAULTNOMINIMODE = 0x20000000 } - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct OpenFileName + public static class ShellIds { - public int lStructSize; - public IntPtr hwndOwner; - public IntPtr hInstance; - public IntPtr lpstrFilter; - public IntPtr lpstrCustomFilter; - public int nMaxCustFilter; - public int nFilterIndex; - public IntPtr lpstrFile; - public int nMaxFile; - public IntPtr lpstrFileTitle; - public int nMaxFileTitle; - public IntPtr lpstrInitialDir; - public IntPtr lpstrTitle; - public OpenFileNameFlags Flags; - private readonly ushort Unused; - private readonly ushort Unused2; - public IntPtr lpstrDefExt; - public IntPtr lCustData; - public IntPtr lpfnHook; - public IntPtr lpTemplateName; - public IntPtr reservedPtr; - public int reservedInt; - public int flagsEx; - } - } + public static readonly Guid OpenFileDialog = Guid.Parse("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7"); + public static readonly Guid SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B"); + public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); + public static readonly Guid IShellItem = Guid.Parse("43826D1E-E718-42EE-BC55-A1E261C37BFE"); + } - [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IFileDialog - { - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - [PreserveSig()] - uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow + [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IFileDialog + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig()] + uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypes(uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] COMDLG_FILTERSPEC[] rgFilterSpec); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileTypeIndex([In] uint iFileType); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypeIndex([In] uint iFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetFileTypeIndex(out uint piFileType); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFileTypeIndex(out uint piFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Unadvise([In] uint dwCookie); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Unadvise([In] uint dwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetOptions([In] uint fos); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOptions([In] uint fos); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetOptions(out uint fos); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetOptions(out uint fos); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Close([MarshalAs(UnmanagedType.Error)] uint hr); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Close([MarshalAs(UnmanagedType.Error)] uint hr); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetClientGuid([In] ref Guid guid); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetClientGuid([In] ref Guid guid); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint ClearClientData(); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint ClearClientData(); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); - } + } + + [ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IFileOpenDialog + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + [PreserveSig()] + uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec); - [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IShellItem - { - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileTypeIndex([In] uint iFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFileTypeIndex(out uint piFileType); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs); + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Unadvise([In] uint dwCookie); - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); - + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint SetOptions([In] uint fos); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetOptions(out uint fos); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Close([MarshalAs(UnmanagedType.Error)] int hr); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetClientGuid([In] ref Guid guid); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void ClearClientData(); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetResults([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppenum); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppsai); + } + + [ComImport, Guid("B63EA76D-1F85-456F-A19C-48159EFA858B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IShellItemArray + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void BindToHandler([In, MarshalAs(UnmanagedType.Interface)] IntPtr pbc, [In] ref Guid rbhid, + [In] ref Guid riid, out IntPtr ppvOut); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetPropertyStore([In] int Flags, [In] ref Guid riid, out IntPtr ppv); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetPropertyDescriptionList([In] ref PROPERTYKEY keyType, [In] ref Guid riid, out IntPtr ppv); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetAttributes([In] SIATTRIBFLAGS dwAttribFlags, [In] uint sfgaoMask, out uint psfgaoAttribs); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetCount(out uint pdwNumItems); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetItemAt([In] uint dwIndex, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void EnumItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenumShellItems); + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct PROPERTYKEY + { + public Guid fmtid; + public uint pid; + } + + public enum SIATTRIBFLAGS + { + SIATTRIBFLAGS_AND = 1, + SIATTRIBFLAGS_APPCOMPAT = 3, + SIATTRIBFLAGS_OR = 2 + } + + [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IShellItem + { + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); + + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct COMDLG_FILTERSPEC + { + [MarshalAs(UnmanagedType.LPWStr)] + public string pszName; + [MarshalAs(UnmanagedType.LPWStr)] + public string pszSpec; + } } - + [Flags] internal enum DropEffect : int { diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index 9b6bf57e92..d555e93a88 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -1,152 +1,94 @@ +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.Win32.Interop; using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Controls.Platform; -using Avalonia.Platform; -using Avalonia.Win32.Interop; namespace Avalonia.Win32 { class SystemDialogImpl : ISystemDialogImpl { - static char[] ToChars(string s) - { - if (s == null) - return null; - var chars = new char[s.Length]; - for (int c = 0; c < s.Length; c++) - chars[c] = s[c]; - return chars; - } - public unsafe Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) { var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; return Task.Factory.StartNew(() => { - var filters = new StringBuilder(); - foreach (var filter in dialog.Filters) - { - var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e)); - filters.Append(filter.Name); - filters.Append(" ("); - filters.Append(extMask); - filters.Append(")"); - filters.Append('\0'); - filters.Append(extMask); - filters.Append('\0'); - } - if (filters.Length == 0) - filters.Append("All files\0*.*\0"); - filters.Append('\0'); + var result = new string[0]; - var filterBuffer = new char[filters.Length]; - filters.CopyTo(0, filterBuffer, 0, filterBuffer.Length); - - var defExt = ToChars((dialog as SaveFileDialog)?.DefaultExtension); - var fileBuffer = new char[256]; - dialog.InitialFileName?.CopyTo(0, fileBuffer, 0, dialog.InitialFileName.Length); + Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog; + Guid iid = UnmanagedMethods.ShellIds.IFileDialog; + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + var frm = (UnmanagedMethods.IFileDialog)unk; - string userSelectedExt = string.Empty; + var openDialog = dialog as OpenFileDialog; + uint options; + frm.GetOptions(out options); + options |= (uint)(UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT); + if (openDialog?.AllowMultiple == true) + options |= (uint)UnmanagedMethods.FOS.FOS_ALLOWMULTISELECT; + frm.SetOptions(options); - var title = ToChars(dialog.Title); - var initialDir = ToChars(dialog.InitialDirectory); + var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? ""; + frm.SetDefaultExtension(defaultExtension); + frm.SetFileName(dialog.InitialFileName ?? ""); + frm.SetTitle(dialog.Title); - fixed (char* pFileBuffer = fileBuffer) - fixed (char* pFilterBuffer = filterBuffer) - fixed (char* pDefExt = defExt) - fixed (char* pInitDir = initialDir) - fixed (char* pTitle = title) + var filters = new List(); + foreach (var filter in dialog.Filters) { - var ofn = new UnmanagedMethods.OpenFileName() - { - hwndOwner = hWnd, - hInstance = IntPtr.Zero, - lCustData = IntPtr.Zero, - nFilterIndex = 0, - Flags = - UnmanagedMethods.OpenFileNameFlags.OFN_EXPLORER | - UnmanagedMethods.OpenFileNameFlags.OFN_HIDEREADONLY, - nMaxCustFilter = 0, - nMaxFile = fileBuffer.Length - 1, - nMaxFileTitle = 0, - lpTemplateName = IntPtr.Zero, - lpfnHook = IntPtr.Zero, - lpstrCustomFilter = IntPtr.Zero, - lpstrDefExt = new IntPtr(pDefExt), - lpstrFile = new IntPtr(pFileBuffer), - lpstrFileTitle = IntPtr.Zero, - lpstrFilter = new IntPtr(pFilterBuffer), - lpstrInitialDir = new IntPtr(pInitDir), - lpstrTitle = new IntPtr(pTitle), - - }; - ofn.lStructSize = Marshal.SizeOf(ofn); - if ((dialog as OpenFileDialog)?.AllowMultiple == true) - ofn.Flags |= UnmanagedMethods.OpenFileNameFlags.OFN_ALLOWMULTISELECT; - - if (dialog is SaveFileDialog) - ofn.Flags |= UnmanagedMethods.OpenFileNameFlags.OFN_NOREADONLYRETURN | - UnmanagedMethods.OpenFileNameFlags.OFN_OVERWRITEPROMPT; - - var pofn = &ofn; - - // We should save the current directory to restore it later. - var currentDirectory = Environment.CurrentDirectory; - - var res = dialog is OpenFileDialog - ? UnmanagedMethods.GetOpenFileName(new IntPtr(pofn)) - : UnmanagedMethods.GetSaveFileName(new IntPtr(pofn)); - - // Restore the old current directory, since GetOpenFileName and GetSaveFileName change it after they're called - Environment.CurrentDirectory = currentDirectory; - - if (!res) - return null; - if (dialog?.Filters.Count > 0) - userSelectedExt = dialog.Filters[ofn.nFilterIndex - 1].Extensions.FirstOrDefault(); + var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e)); + filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = filter.Name, pszSpec = extMask }); } - var cStart = 0; - string dir = null; - var files = new List(); - for (var c = 0; c < fileBuffer.Length; c++) + if (filters.Count == 0) + filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = "All files", pszSpec = "*.*" }); + + frm.SetFileTypes((uint)filters.Count, filters.ToArray()); + frm.SetFileTypeIndex(0); + + if (dialog.InitialDirectory != null) { - if (fileBuffer[c] == 0) + UnmanagedMethods.IShellItem directoryShellItem; + Guid riid = UnmanagedMethods.ShellIds.IShellItem; + if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { - //Encountered double zero char - if (cStart == c) - break; - - var s = new string(fileBuffer, cStart, c - cStart); - if (dir == null) - dir = s; - else - files.Add(s); - cStart = c + 1; + frm.SetFolder(directoryShellItem); + frm.SetDefaultFolder(directoryShellItem); } } - if (files.Count == 0) + + if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK) { - if (dialog is SaveFileDialog) + if (openDialog?.AllowMultiple == true) { - if (string.IsNullOrWhiteSpace(Path.GetExtension(dir)) && - !string.IsNullOrWhiteSpace(userSelectedExt) && - !userSelectedExt.Contains("*")) - dir = Path.ChangeExtension(dir, userSelectedExt); + UnmanagedMethods.IShellItemArray shellItemArray; + ((UnmanagedMethods.IFileOpenDialog)frm).GetResults(out shellItemArray); + uint count; + shellItemArray.GetCount(out count); + result = new string[count]; + for (uint i = 0; i < count; i++) + { + UnmanagedMethods.IShellItem shellItem; + shellItemArray.GetItemAt(i, out shellItem); + result[i] = GetAbsoluteFilePath(shellItem); + } + } + else + { + UnmanagedMethods.IShellItem shellItem; + if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + result = new string[] { GetAbsoluteFilePath(shellItem) }; + } } - - return new[] { dir }; } - return files.Select(f => Path.Combine(dir, f)).ToArray(); + return result; }); } @@ -157,11 +99,11 @@ namespace Avalonia.Win32 string result = string.Empty; var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; - var clsid = Guid.Parse("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7"); - var iid = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); + Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; + Guid iid = UnmanagedMethods.ShellIds.IFileDialog; UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); - var frm = (IFileDialog)unk; + var frm = (UnmanagedMethods.IFileDialog)unk; uint options; frm.GetOptions(out options); options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT); @@ -169,8 +111,8 @@ namespace Avalonia.Win32 if (dialog.InitialDirectory != null) { - IShellItem directoryShellItem; - var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem + UnmanagedMethods.IShellItem directoryShellItem; + Guid riid = UnmanagedMethods.ShellIds.IShellItem; if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { frm.SetFolder(directoryShellItem); @@ -179,8 +121,8 @@ namespace Avalonia.Win32 if (dialog.DefaultDirectory != null) { - IShellItem directoryShellItem; - var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem + UnmanagedMethods.IShellItem directoryShellItem; + Guid riid = UnmanagedMethods.ShellIds.IShellItem; if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.DefaultDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { frm.SetDefaultFolder(directoryShellItem); @@ -189,29 +131,35 @@ namespace Avalonia.Win32 if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK) { - IShellItem shellItem; + UnmanagedMethods.IShellItem shellItem; if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) { - IntPtr pszString; - if (shellItem.GetDisplayName(UnmanagedMethods.SIGDN_FILESYSPATH, out pszString) == (uint)UnmanagedMethods.HRESULT.S_OK) - { - if (pszString != IntPtr.Zero) - { - try - { - result = Marshal.PtrToStringAuto(pszString); - } - finally - { - Marshal.FreeCoTaskMem(pszString); - } - } - } + result = GetAbsoluteFilePath(shellItem); } } return result; }); } + + private string GetAbsoluteFilePath(UnmanagedMethods.IShellItem shellItem) + { + IntPtr pszString; + if (shellItem.GetDisplayName(UnmanagedMethods.SIGDN_FILESYSPATH, out pszString) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + if (pszString != IntPtr.Zero) + { + try + { + return Marshal.PtrToStringAuto(pszString); + } + finally + { + Marshal.FreeCoTaskMem(pszString); + } + } + } + return ""; + } } } From cbca7beefde6c69bc21be5bf5d0eb8919278a057 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 6 Jun 2018 20:53:27 +0200 Subject: [PATCH 033/149] Initial --- .../Media/PathMarkupParser.cs | 805 +++++++++++------- src/Avalonia.Visuals/Media/StreamGeometry.cs | 10 +- .../Media/PathMarkupParserTests.cs | 127 ++- 3 files changed, 610 insertions(+), 332 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 9e4a3cbeae..0307701e82 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -5,50 +5,46 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Text; +using System.Text.RegularExpressions; namespace Avalonia.Media { /// /// Parses a path markup string. /// - public class PathMarkupParser + public class PathMarkupParser : IDisposable { - private static readonly Dictionary Commands = new Dictionary - { - { 'F', Command.FillRule }, - { 'M', Command.Move }, - { 'L', Command.Line }, - { 'H', Command.HorizontalLine }, - { 'V', Command.VerticalLine }, - { 'Q', Command.QuadraticBezierCurve }, - { 'T', Command.SmoothQuadraticBezierCurve }, - { 'C', Command.CubicBezierCurve }, - { 'S', Command.SmoothCubicBezierCurve }, - { 'A', Command.Arc }, - { 'Z', Command.Close }, - }; - - private static readonly Dictionary FillRules = new Dictionary - { - {'0', FillRule.EvenOdd }, - {'1', FillRule.NonZero } - }; + private static readonly string s_separatorPattern; - private readonly StreamGeometryContext _context; + private Point _currentPoint; + private Point? _previousControlPoint; + private PathGeometry _currentGeometry; + private PathFigure _currentFigure; + private bool _isDisposed; - /// - /// Initializes a new instance of the class. - /// - /// The context for the geometry. - public PathMarkupParser(StreamGeometryContext context) + private static readonly Dictionary s_commands = + new Dictionary + { + { 'F', Command.FillRule }, + { 'M', Command.Move }, + { 'L', Command.Line }, + { 'H', Command.HorizontalLine }, + { 'V', Command.VerticalLine }, + { 'Q', Command.QuadraticBezierCurve }, + { 'T', Command.SmoothQuadraticBezierCurve }, + { 'C', Command.CubicBezierCurve }, + { 'S', Command.SmoothCubicBezierCurve }, + { 'A', Command.Arc }, + { 'Z', Command.Close }, + }; + + static PathMarkupParser() { - _context = context; + s_separatorPattern = CreatesSeparatorPattern(); } - /// - /// Defines the command currently being processed. - /// private enum Command { None, @@ -62,358 +58,581 @@ namespace Avalonia.Media SmoothCubicBezierCurve, SmoothQuadraticBezierCurve, Arc, - Close, + Close } - /// - /// Parses the specified markup string. - /// - /// The markup string. - public void Parse(string s) + public PathGeometry Parse(string s) { - bool openFigure = false; + _currentGeometry = new PathGeometry(); + + var tokens = ParseTokens(s); - using (StringReader reader = new StringReader(s)) + return CreateGeometry(tokens); + } + + void IDisposable.Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) { - Command command = Command.None; - Point point = new Point(); - bool relative = false; - Point? previousControlPoint = null; + return; + } + + if (disposing) + { + _currentFigure = null; + + _currentGeometry = null; + } + + _isDisposed = true; + } + + private static string CreatesSeparatorPattern() + { + var stringBuilder = new StringBuilder(); + + foreach (var command in s_commands.Keys) + { + stringBuilder.Append(command); + + stringBuilder.Append(char.ToLower(command)); + } + + return @"(?=[" + stringBuilder + "])"; + } + + private static IEnumerable ParseTokens(string s) + { + return Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t)).Select(CommandToken.Parse); + } + + private static Point MirrorControlPoint(Point controlPoint, Point center) + { + var dir = controlPoint - center; + + return center + -dir; + } - while (ReadCommand(reader, ref command, ref relative)) + private PathGeometry CreateGeometry(IEnumerable commandTokens) + { + _currentGeometry = new PathGeometry(); + + _currentPoint = new Point(); + + foreach (var commandToken in commandTokens) + { + try { - switch (command) + while (true) { - case Command.FillRule: - _context.SetFillRule(ReadFillRule(reader)); - previousControlPoint = null; - break; - - case Command.Move: - if (openFigure) - { - _context.EndFigure(false); - } - - point = ReadPoint(reader, point, relative); - _context.BeginFigure(point, true); - openFigure = true; - previousControlPoint = null; - break; - - case Command.Line: - point = ReadPoint(reader, point, relative); - _context.LineTo(point); - previousControlPoint = null; - break; - - case Command.HorizontalLine: - if (!relative) - { - point = point.WithX(ReadDouble(reader)); - } - else - { - point = new Point(point.X + ReadDouble(reader), point.Y); - } - - _context.LineTo(point); - previousControlPoint = null; - break; - - case Command.VerticalLine: - if (!relative) - { - point = point.WithY(ReadDouble(reader)); - } - else - { - point = new Point(point.X, point.Y + ReadDouble(reader)); - } - - _context.LineTo(point); - previousControlPoint = null; - break; - - case Command.QuadraticBezierCurve: - { - Point handle = ReadPoint(reader, point, relative); - previousControlPoint = handle; - ReadSeparator(reader); - point = ReadPoint(reader, point, relative); - _context.QuadraticBezierTo(handle, point); + switch (commandToken.Command) + { + case Command.None: + break; + case Command.FillRule: + SetFillRule(commandToken); + break; + case Command.Move: + AddMove(commandToken); + break; + case Command.Line: + AddLine(commandToken); + break; + case Command.HorizontalLine: + AddHorizontalLine(commandToken); + break; + case Command.VerticalLine: + AddVerticalLine(commandToken); + break; + case Command.CubicBezierCurve: + AddCubicBezierCurve(commandToken); + break; + case Command.QuadraticBezierCurve: + AddQuadraticBezierCurve(commandToken); break; - } - - case Command.SmoothQuadraticBezierCurve: - { - Point end = ReadPoint(reader, point, relative); - - if(previousControlPoint != null) - previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point); - - _context.QuadraticBezierTo(previousControlPoint ?? point, end); - point = end; + case Command.SmoothCubicBezierCurve: + AddSmoothCubicBezierCurve(commandToken); break; - } - - case Command.CubicBezierCurve: - { - Point point1 = ReadPoint(reader, point, relative); - ReadSeparator(reader); - Point point2 = ReadPoint(reader, point, relative); - previousControlPoint = point2; - ReadSeparator(reader); - point = ReadPoint(reader, point, relative); - _context.CubicBezierTo(point1, point2, point); + case Command.SmoothQuadraticBezierCurve: + AddSmoothQuadraticBezierCurve(commandToken); break; - } - - case Command.SmoothCubicBezierCurve: - { - Point point2 = ReadPoint(reader, point, relative); - ReadSeparator(reader); - Point end = ReadPoint(reader, point, relative); - - if(previousControlPoint != null) - previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point); - - _context.CubicBezierTo(previousControlPoint ?? point, point2, end); - previousControlPoint = point2; - point = end; + case Command.Arc: + AddArc(commandToken); break; - } - - case Command.Arc: - { - Size size = ReadSize(reader); - ReadSeparator(reader); - double rotationAngle = ReadDouble(reader); - ReadSeparator(reader); - bool isLargeArc = ReadBool(reader); - ReadSeparator(reader); - SweepDirection sweepDirection = ReadBool(reader) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; - ReadSeparator(reader); - point = ReadPoint(reader, point, relative); - - _context.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection); - previousControlPoint = null; + case Command.Close: + CloseFigure(); break; - } + default: + throw new NotSupportedException("Unsupported command"); + } - case Command.Close: - _context.EndFigure(true); - openFigure = false; - previousControlPoint = null; - break; + if (commandToken.HasImplicitCommands) + { + continue; + } - default: - throw new NotSupportedException("Unsupported command"); + break; } } - - if (openFigure) + catch (InvalidDataException) { - _context.EndFigure(false); + break; + } + catch (NotSupportedException) + { + break; } } + + return _currentGeometry; } - private Point MirrorControlPoint(Point controlPoint, Point center) + private void SetFillRule(CommandToken commandToken) { - Point dir = (controlPoint - center); - return center + -dir; + _currentGeometry.FillRule = commandToken.ReadFillRule(); } - private static bool ReadCommand( - StringReader reader, - ref Command command, - ref bool relative) + private void CloseFigure() { - ReadWhitespace(reader); + if (_currentFigure != null && !_currentFigure.IsClosed) + { + _currentFigure.IsClosed = true; + } - int i = reader.Peek(); + _previousControlPoint = null; - if (i == -1) + _currentFigure = null; + } + + private void CreateFigure() + { + _currentFigure = new PathFigure { - return false; + StartPoint = _currentPoint, + IsClosed = false + }; + + _currentGeometry.Figures.Add(_currentFigure); + } + + private void AddSegment(PathSegment segment) + { + if (_currentFigure == null) + { + CreateFigure(); } - else + + _currentFigure.Segments.Add(segment); + } + + private void AddMove(CommandToken commandToken) + { + var currentPoint = commandToken.ReadPoint(); + + _currentPoint = currentPoint; + + CreateFigure(); + + if (!commandToken.HasImplicitCommands) { - char c = (char)i; - Command next = Command.None; + return; + } - if (!Commands.TryGetValue(char.ToUpperInvariant(c), out next)) + while (commandToken.HasImplicitCommands) + { + AddLine(commandToken); + + if (commandToken.IsRelative) { - if ((char.IsDigit(c) || c == '.' || c == '+' || c == '-') && - (command != Command.None)) - { - return true; - } - else - { - throw new InvalidDataException("Unexpected path command '" + c + "'."); - } + continue; } - command = next; - relative = char.IsLower(c); - reader.Read(); - return true; + _currentPoint = currentPoint; + + CreateFigure(); } } - private static FillRule ReadFillRule(StringReader reader) + private void AddLine(CommandToken commandToken) { - int i = reader.Read(); - if (i == -1) - { - throw new InvalidDataException("Invalid fill rule"); - } - char c = (char)i; - FillRule rule; + _currentPoint = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - if (!FillRules.TryGetValue(c, out rule)) + var lineSegment = new LineSegment { - throw new InvalidDataException("Invalid fill rule"); - } + Point = _currentPoint + }; - return rule; + AddSegment(lineSegment); } - private static double ReadDouble(StringReader reader) + private void AddHorizontalLine(CommandToken commandToken) { - ReadWhitespace(reader); + _currentPoint = commandToken.IsRelative + ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) + : _currentPoint.WithX(commandToken.ReadDouble()); - // TODO: Handle Infinity, NaN and scientific notation. - StringBuilder b = new StringBuilder(); - bool readSign = false; - bool readPoint = false; - bool readExponent = false; - int i; + var lineSegment = new LineSegment + { + Point = _currentPoint + }; - while ((i = reader.Peek()) != -1) + AddSegment(lineSegment); + } + + private void AddVerticalLine(CommandToken commandToken) + { + _currentPoint = commandToken.IsRelative + ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) + : _currentPoint.WithY(commandToken.ReadDouble()); + + var lineSegment = new LineSegment { - char c = char.ToUpperInvariant((char)i); + Point = _currentPoint + }; - if (((c == '+' || c == '-') && !readSign) || - (c == '.' && !readPoint) || - (c == 'E' && !readExponent) || - char.IsDigit(c)) - { - if (b.Length != 0 && !readExponent && c == '-') - break; - - b.Append(c); - reader.Read(); + AddSegment(lineSegment); + } - if (!readSign) - { - readSign = c == '+' || c == '-'; - } + private void AddCubicBezierCurve(CommandToken commandToken) + { + var point1 = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - if (!readPoint) - { - readPoint = c == '.'; - } + var point2 = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - if (c == 'E') - { - readSign = false; - readExponent = true; - } - } - else - { - break; - } - } + _previousControlPoint = point2; + + var point3 = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + var bezierSegment = new BezierSegment + { + Point1 = point1, + Point2 = point2, + Point3 = point3 + }; + + AddSegment(bezierSegment); - return double.Parse(b.ToString(), CultureInfo.InvariantCulture); + _currentPoint = point3; } - private static Point ReadPoint(StringReader reader, Point current, bool relative) + private void AddQuadraticBezierCurve(CommandToken commandToken) { - if (!relative) + var start = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + _previousControlPoint = start; + + var end = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + var quadraticBezierSegment = new QuadraticBezierSegment { - current = new Point(); - } + Point1 = start, + Point2 = end + }; + + AddSegment(quadraticBezierSegment); - ReadWhitespace(reader); - double x = current.X + ReadDouble(reader); - ReadSeparator(reader); - double y = current.Y + ReadDouble(reader); - return new Point(x, y); + _currentPoint = end; } - private static Size ReadSize(StringReader reader) + private void AddSmoothCubicBezierCurve(CommandToken commandToken) { - ReadWhitespace(reader); - double x = ReadDouble(reader); - ReadSeparator(reader); - double y = ReadDouble(reader); - return new Size(x, y); + var point2 = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + var end = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + if (_previousControlPoint != null) + { + _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); + } + + var bezierSegment = + new BezierSegment { Point1 = _previousControlPoint ?? _currentPoint, Point2 = point2, Point3 = end }; + + AddSegment(bezierSegment); + + _previousControlPoint = point2; + + _currentPoint = end; } - private static bool ReadBool(StringReader reader) + private void AddSmoothQuadraticBezierCurve(CommandToken commandToken) { - return ReadDouble(reader) != 0; + var end = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + if (_previousControlPoint != null) + { + _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); + } + + var quadraticBezierSegment = new QuadraticBezierSegment + { + Point1 = _previousControlPoint ?? _currentPoint, + Point2 = end + }; + + AddSegment(quadraticBezierSegment); + + _currentPoint = end; } - private static Point ReadRelativePoint(StringReader reader, Point lastPoint) + private void AddArc(CommandToken commandToken) { - ReadWhitespace(reader); - double x = ReadDouble(reader); - ReadSeparator(reader); - double y = ReadDouble(reader); - return new Point(lastPoint.X + x, lastPoint.Y + y); + var size = commandToken.ReadSize(); + + var rotationAngle = commandToken.ReadDouble(); + + var isLargeArc = commandToken.ReadBool(); + + var sweepDirection = commandToken.ReadBool() ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; + + var end = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); + + var arcSegment = new ArcSegment + { + Size = size, + RotationAngle = rotationAngle, + IsLargeArc = isLargeArc, + SweepDirection = sweepDirection, + Point = end + }; + + AddSegment(arcSegment); + + _currentPoint = end; + + _previousControlPoint = null; } - private static void ReadSeparator(StringReader reader) + private class CommandToken { - int i; - bool readComma = false; + private const string ArgumentExpression = @"-?[0-9]*\.?\d+"; - while ((i = reader.Peek()) != -1) + private CommandToken(Command command, bool isRelative, IEnumerable arguments) { - char c = (char)i; + Command = command; + + IsRelative = isRelative; + + Arguments = new List(arguments); + } + + public Command Command { get; } + + public bool IsRelative { get; } - if (char.IsWhiteSpace(c)) + public bool HasImplicitCommands + { + get { - reader.Read(); + if (CurrentPosition == 0 && Arguments.Count > 0) + { + return true; + } + + return CurrentPosition < Arguments.Count - 1; } - else if (c == ',') + } + + private int CurrentPosition { get; set; } + + private List Arguments { get; } + + public static CommandToken Parse(string s) + { + using (var reader = new StringReader(s)) { - if (readComma) + var command = Command.None; + + var isRelative = false; + + if (!ReadCommand(reader, ref command, ref isRelative)) { - throw new InvalidDataException("Unexpected ','."); + throw new InvalidDataException("No path command declared."); } - readComma = true; - reader.Read(); + var commandArguments = reader.ReadToEnd(); + + var argumentMatches = Regex.Matches(commandArguments, ArgumentExpression); + + var arguments = new List(); + + foreach (Match match in argumentMatches) + { + arguments.Add(match.Value); + } + + return new CommandToken(command, isRelative, arguments); } - else + } + + public FillRule ReadFillRule() + { + if (CurrentPosition == Arguments.Count) { - break; + throw new InvalidDataException("Invalid fill rule"); + } + + var value = Arguments[CurrentPosition]; + + CurrentPosition++; + + switch (value) + { + case "0": + { + return FillRule.EvenOdd; + } + + case "1": + { + return FillRule.NonZero; + } + + default: + throw new InvalidDataException("Invalid fill rule"); } } - } - private static void ReadWhitespace(StringReader reader) - { - int i; + public bool ReadBool() + { + if (CurrentPosition == Arguments.Count) + { + throw new InvalidDataException("Invalid boolean value"); + } + + var value = Arguments[CurrentPosition]; - while ((i = reader.Peek()) != -1) + CurrentPosition++; + + switch (value) + { + case "1": + { + return true; + } + + case "0": + { + return false; + } + + default: + throw new InvalidDataException("Invalid boolean value"); + } + } + + public double ReadDouble() { - char c = (char)i; + if (CurrentPosition == Arguments.Count) + { + throw new InvalidDataException("Invalid double value"); + } + + var value = Arguments[CurrentPosition]; + + CurrentPosition++; + + return double.Parse(value, CultureInfo.InvariantCulture); + } - if (char.IsWhiteSpace(c)) + public Size ReadSize() + { + var width = ReadDouble(); + + var height = ReadDouble(); + + return new Size(width, height); + } + + public Point ReadPoint() + { + var x = ReadDouble(); + + var y = ReadDouble(); + + return new Point(x, y); + } + + public Point ReadRelativePoint(Point origin) + { + var x = ReadDouble(); + + var y = ReadDouble(); + + return new Point(origin.X + x, origin.Y + y); + } + + private static bool ReadCommand( + TextReader reader, + ref Command command, + ref bool relative) + { + ReadWhitespace(reader); + + var i = reader.Peek(); + + if (i == -1) { - reader.Read(); + return false; } - else + + var c = (char)i; + + if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next)) { - break; + throw new InvalidDataException("Unexpected path command '" + c + "'."); + } + + command = next; + + relative = char.IsLower(c); + + reader.Read(); + + return true; + } + + private static void ReadWhitespace(TextReader reader) + { + int i; + + while ((i = reader.Peek()) != -1) + { + var c = (char)i; + + if (char.IsWhiteSpace(c)) + { + reader.Read(); + } + else + { + break; + } } } } diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs index 9848a649aa..1983740375 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometry.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs @@ -35,14 +35,10 @@ namespace Avalonia.Media /// A . public static new StreamGeometry Parse(string s) { - StreamGeometry result = new StreamGeometry(); - - using (StreamGeometryContext ctx = result.Open()) + using (var parser = new PathMarkupParser()) { - PathMarkupParser parser = new PathMarkupParser(ctx); - parser.Parse(s); - return result; - } + return parser.Parse(s); + } } /// diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index e63d23283c..d7eb6129ac 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -8,53 +8,128 @@ using Xunit; namespace Avalonia.Visuals.UnitTests.Media { + using System.Linq; + public class PathMarkupParserTests { [Fact] public void Parses_Move() { - using (AvaloniaLocator.EnterScope()) - { - var result = new Mock(); - - var parser = PrepareParser(result); + using (var parser = new PathMarkupParser()) + { + var geometry = parser.Parse("M10 10"); - parser.Parse("M10 10"); + var figure = geometry.Figures.First(); - result.Verify(x => x.BeginFigure(new Point(10, 10), true)); + Assert.Equal(new Point(10, 10), figure.StartPoint); } } [Fact] public void Parses_Line() { - using (AvaloniaLocator.EnterScope()) + using (var parser = new PathMarkupParser()) { - var result = new Mock(); + var geometry = parser.Parse("M0 0L10 10"); - var parser = PrepareParser(result); + var figure = geometry.Figures.First(); - parser.Parse("M0 0L10 10"); + var segment = figure.Segments.First(); - result.Verify(x => x.LineTo(new Point(10, 10))); + Assert.IsType(segment); + + var lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(10, 10), lineSegment.Point); } } [Fact] public void Parses_Close() { - using (AvaloniaLocator.EnterScope()) + using (var parser = new PathMarkupParser()) { - var result = new Mock(); + var geometry = parser.Parse("M0 0L10 10z"); + + var figure = geometry.Figures.First(); + + Assert.True(figure.IsClosed); + } + } + + [Fact] + public void Parses_FillMode_Before_Move() + { + using (var parser = new PathMarkupParser()) + { + var geometry = parser.Parse("F 1M0,0"); + + Assert.Equal(FillRule.NonZero, geometry.FillRule); + } + } + + [Theory] + [InlineData("M0 0 10 10 20 20")] + [InlineData("M0,0 10,10 20,20")] + [InlineData("M0,0,10,10,20,20")] + public void Parses_Implicit_Line_Command_After_Move(string pathData) + { + using (var parser = new PathMarkupParser()) + { + var geometry = parser.Parse(pathData); + + var figure = geometry.Figures[0]; + + var segment = figure.Segments[0]; + + Assert.IsType(segment); - var parser = PrepareParser(result); + var lineSegment = (LineSegment)segment; - parser.Parse("M0 0L10 10z"); + Assert.Equal(new Point(10, 10), lineSegment.Point); - result.Verify(x => x.EndFigure(true)); + figure = geometry.Figures[1]; + + segment = figure.Segments[0]; + + Assert.IsType(segment); + + lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(20, 20), lineSegment.Point); } } + [Theory] + [InlineData("m0 0 10 10 20 20")] + [InlineData("m0,0 10,10 20,20")] + [InlineData("m0,0,10,10,20,20")] + public void Parses_Implicit_Line_Command_After_Relative_Move(string pathData) + { + using (var parser = new PathMarkupParser()) + { + var geometry = parser.Parse(pathData); + + var figure = geometry.Figures[0]; + + var segment = figure.Segments[0]; + + Assert.IsType(segment); + + var lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(10, 10), lineSegment.Point); + + segment = figure.Segments[1]; + + Assert.IsType(segment); + + lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(30, 30), lineSegment.Point); + } + } + [Theory] [InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z")] // issue #1107 [InlineData("M0 0L10 10z")] @@ -75,29 +150,17 @@ namespace Avalonia.Visuals.UnitTests.Media ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" + ".9063 10.1309 30.9062 16.6309 19.9063 Z ")] [InlineData( - "F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 " + - "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 " + + "F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 " + + "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 " + "12.461,8.046C14.45,8.278,16,9.949,16,12")] public void Should_Parse(string pathData) { - using (AvaloniaLocator.EnterScope()) + using (var parser = new PathMarkupParser()) { - var parser = PrepareParser(); - parser.Parse(pathData); Assert.True(true); } } - - private static PathMarkupParser PrepareParser(Mock implMock = null) - { - AvaloniaLocator.CurrentMutable - .Bind() - .ToConstant(Mock.Of()); - - return new PathMarkupParser( - new StreamGeometryContext(implMock != null ? implMock.Object : Mock.Of())); - } } } \ No newline at end of file From e576ec178c95ccbd8a5772754ad1c30d87465d26 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 6 Jun 2018 21:06:05 +0200 Subject: [PATCH 034/149] 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 43e6bd2d492cbeff59e06c2080d8e03cbc02171e Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 6 Jun 2018 14:46:54 -0500 Subject: [PATCH 035/149] Switch off from deprecated APIs in Avalonia.MonoMac. --- src/OSX/Avalonia.MonoMac/SystemDialogsImpl.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/OSX/Avalonia.MonoMac/SystemDialogsImpl.cs b/src/OSX/Avalonia.MonoMac/SystemDialogsImpl.cs index 4226668119..04db728a2b 100644 --- a/src/OSX/Avalonia.MonoMac/SystemDialogsImpl.cs +++ b/src/OSX/Avalonia.MonoMac/SystemDialogsImpl.cs @@ -7,6 +7,7 @@ using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Platform; using MonoMac.AppKit; +using MonoMac.Foundation; namespace Avalonia.MonoMac { @@ -24,9 +25,9 @@ namespace Avalonia.MonoMac else { if (panel is NSOpenPanel openPanel) - tcs.SetResult(openPanel.Filenames); + tcs.SetResult(openPanel.Urls.Select(url => url.AbsoluteString).ToArray()); else - tcs.SetResult(new[] { panel.Filename }); + tcs.SetResult(new[] { panel.Url.AbsoluteString }); } panel.OrderOut(panel); keyWindow?.MakeKeyAndOrderFront(keyWindow); @@ -62,7 +63,7 @@ namespace Avalonia.MonoMac panel = new NSSavePanel(); panel.Title = panel.Title; if (dialog.InitialDirectory != null) - panel.Directory = dialog.InitialDirectory; + panel.DirectoryUrl = new NSUrl(dialog.InitialDirectory); if (dialog.InitialFileName != null) panel.NameFieldStringValue = dialog.InitialFileName; if (dialog.Filters?.Count > 0) @@ -84,7 +85,7 @@ namespace Avalonia.MonoMac CanChooseFiles = false }; if (dialog.DefaultDirectory != null) - panel.Directory = dialog.DefaultDirectory; + panel.DirectoryUrl = new NSUrl(dialog.DefaultDirectory); return (await RunPanel(panel, parent))?.FirstOrDefault(); } } From 8d004c5ae817983df64953b230e3400f8f05c3e9 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 6 Jun 2018 16:17:32 -0500 Subject: [PATCH 036/149] Reduce output of Inspect build step. --- build.cake | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/build.cake b/build.cake index a64fbf0fba..632b572e69 100644 --- a/build.cake +++ b/build.cake @@ -366,8 +366,13 @@ Task("Inspect") "src\\markup\\avalonia.markup.xaml\\portablexaml\\portable.xaml.github"}; Information("Running code inspections"); - StartProcess(Context.Tools.Resolve("inspectcode.exe"), - new ProcessSettings{ Arguments = "--output=artifacts\\inspectcode.xml --profile=Avalonia.sln.DotSettings Avalonia.sln" }); + var exitCode = StartProcess(Context.Tools.Resolve("inspectcode.exe"), + new ProcessSettings + { + Arguments = "--output=artifacts\\inspectcode.xml --profile=Avalonia.sln.DotSettings Avalonia.sln", + RedirectStandardOutput = true + }); + Information("Analyzing report"); var doc = XDocument.Parse(System.IO.File.ReadAllText("artifacts\\inspectcode.xml")); var failBuild = false; From bc882f5b1137aedd9cc85c1e79480c66528a46fa Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 6 Jun 2018 16:38:11 -0500 Subject: [PATCH 037/149] Add back NetFX.props framework path overriding. --- tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj | 3 ++- tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index 7966cac845..27f3223c6c 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -1,11 +1,12 @@  - net47 + net461 + diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index d86b27e804..24cc6db7f3 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -35,7 +35,5 @@ - - - + \ No newline at end of file From 08ab688edd4b5115713232b874affe62daede8dd Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 6 Jun 2018 16:52:15 -0500 Subject: [PATCH 038/149] Build Avalonia.DotNetFrameworkRuntime in the NetCoreOnly build. --- Avalonia.sln | 1 + 1 file changed, 1 insertion(+) diff --git a/Avalonia.sln b/Avalonia.sln index 54f6f5e7e7..86a5411695 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1813,6 +1813,7 @@ Global {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU + {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Debug|x86.ActiveCfg = Debug|Any CPU {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Debug|x86.Build.0 = Debug|Any CPU {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU From f985fc819e57d93dc85e6b17fc65ddcc74b316d6 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 6 Jun 2018 17:59:34 -0500 Subject: [PATCH 039/149] Fix LeakTests path --- build.cake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.cake b/build.cake index 632b572e69..bf3ae41b58 100644 --- a/build.cake +++ b/build.cake @@ -240,7 +240,7 @@ Task("Run-Leak-Tests") .Append(Context.Tools.Resolve("xunit.console.x86.exe").FullPath) .Append("--propagate-exit-code") .Append("--") - .Append("tests\\Avalonia.LeakTests\\bin\\Release\\net47\\Avalonia.LeakTests.dll"), + .Append("tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll"), Timeout = 120000 }); From 4ec647b870173c545a12def947d92ab44f5be515 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 6 Jun 2018 19:50:32 -0500 Subject: [PATCH 040/149] Make our unit tests support library (Avalonia.UnitTests) target netstandard2.0 so we don't have to worry about targetting .NET Framework on linux. --- Avalonia.sln | 1 - src/Avalonia.Controls/AppBuilderBase.cs | 11 +--------- .../AppBuilderTests.cs | 5 +---- .../Avalonia.DesignerSupport.TestApp.csproj | 2 +- .../DesignerSupportTests.cs | 8 ++++++++ .../FullLayoutTests.cs | 1 + .../Avalonia.UnitTests.csproj | 20 ++----------------- tests/Avalonia.UnitTests/RuntimeInfo.cs | 16 +++++++++++++++ tests/Avalonia.UnitTests/TestServices.cs | 14 +++++++++++++ tests/Avalonia.UnitTests/app.config | 11 ---------- 10 files changed, 44 insertions(+), 45 deletions(-) create mode 100644 tests/Avalonia.UnitTests/RuntimeInfo.cs delete mode 100644 tests/Avalonia.UnitTests/app.config diff --git a/Avalonia.sln b/Avalonia.sln index 86a5411695..54f6f5e7e7 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1813,7 +1813,6 @@ Global {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU - {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Debug|x86.ActiveCfg = Debug|Any CPU {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Debug|x86.Build.0 = Debug|Any CPU {4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 7af3deef34..51b690ece9 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -209,16 +209,7 @@ namespace Avalonia.Controls public TAppBuilder UseAvaloniaModules() => AfterSetup(builder => SetupAvaloniaModules()); - private bool CheckSetup { get; set; } = true; - - /// - /// Set this AppBuilder to ignore the setup check. Used for testing purposes. - /// - internal TAppBuilder IgnoreSetupCheck() - { - CheckSetup = false; - return Self; - } + protected virtual bool CheckSetup => true; private void SetupAvaloniaModules() { diff --git a/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs b/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs index 60c53d126c..fae08d37b7 100644 --- a/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Xunit; using Avalonia.Controls.UnitTests; using Avalonia.Platform; +using Avalonia.UnitTests; [assembly: ExportAvaloniaModule("DefaultModule", typeof(AppBuilderTests.DefaultModule))] [assembly: ExportAvaloniaModule("RenderingModule", typeof(AppBuilderTests.Direct2DModule), ForRenderingSubsystem = "Direct2D1")] @@ -65,7 +66,6 @@ namespace Avalonia.Controls.UnitTests { ResetModuleLoadStates(); AppBuilder.Configure() - .IgnoreSetupCheck() .UseWindowingSubsystem(() => { }) .UseRenderingSubsystem(() => { }) .UseAvaloniaModules() @@ -82,7 +82,6 @@ namespace Avalonia.Controls.UnitTests { ResetModuleLoadStates(); var builder = AppBuilder.Configure() - .IgnoreSetupCheck() .UseWindowingSubsystem(() => { }) .UseRenderingSubsystem(() => { }, "Direct2D1"); builder.UseAvaloniaModules().SetupWithoutStarting(); @@ -92,7 +91,6 @@ namespace Avalonia.Controls.UnitTests ResetModuleLoadStates(); builder = AppBuilder.Configure() - .IgnoreSetupCheck() .UseWindowingSubsystem(() => { }) .UseRenderingSubsystem(() => { }, "Skia"); builder.UseAvaloniaModules().SetupWithoutStarting(); @@ -109,7 +107,6 @@ namespace Avalonia.Controls.UnitTests { ResetModuleLoadStates(); var builder = AppBuilder.Configure() - .IgnoreSetupCheck() .UseWindowingSubsystem(() => { }) .UseRenderingSubsystem(() => { }, "TBD"); builder.UseAvaloniaModules().SetupWithoutStarting(); diff --git a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj index 76017f96bd..dd33ee831d 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj +++ b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp2.0 + netcoreapp2.0 diff --git a/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs b/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs index 5386877876..5220d539d9 100644 --- a/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs +++ b/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs @@ -19,6 +19,12 @@ namespace Avalonia.DesignerSupport.Tests public class DesignerSupportTests { private const string DesignerAppPath = "../../../../../src/tools/Avalonia.Designer.HostApp/bin/$BUILD/netcoreapp2.0/Avalonia.Designer.HostApp.dll"; + private readonly Xunit.Abstractions.ITestOutputHelper outputHelper; + + public DesignerSupportTests(Xunit.Abstractions.ITestOutputHelper outputHelper) + { + this.outputHelper = outputHelper; + } [SkippableTheory, InlineData( @@ -73,6 +79,8 @@ namespace Avalonia.DesignerSupport.Tests } else if (msg is UpdateXamlResultMessage result) { + if (result.Error != null) + outputHelper.WriteLine(result.Error); handle = result.Handle != null ? long.Parse(result.Handle) : 0; resultMessageReceivedToken.Cancel(); conn.Dispose(); diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index d4df32a4b3..053c77b911 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -21,6 +21,7 @@ using Xunit; using Avalonia.Media; using System; using System.Collections.Generic; +using Avalonia.UnitTests; namespace Avalonia.Layout.UnitTests { diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index 24cc6db7f3..c189bbbe66 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -1,22 +1,10 @@  - netcoreapp2.0;net461 + netstandard2.0 false Library - - - - - - - - - - - - @@ -29,11 +17,7 @@ - - - - - + \ No newline at end of file diff --git a/tests/Avalonia.UnitTests/RuntimeInfo.cs b/tests/Avalonia.UnitTests/RuntimeInfo.cs new file mode 100644 index 0000000000..eb5781a725 --- /dev/null +++ b/tests/Avalonia.UnitTests/RuntimeInfo.cs @@ -0,0 +1,16 @@ +using Avalonia.Platform; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace Avalonia.Shared.PlatformSupport +{ + internal partial class StandardRuntimePlatform : IRuntimePlatform + { + public RuntimePlatformInfo GetRuntimeInfo() + { + return new RuntimePlatformInfo(); + } + } +} diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 394dcbaa22..8414fe7cd5 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -14,6 +14,8 @@ using Avalonia.Themes.Default; using Avalonia.Rendering; using System.Reactive.Concurrency; using System.Collections.Generic; +using Avalonia.Controls; +using System.Reflection; namespace Avalonia.UnitTests { @@ -178,4 +180,16 @@ namespace Avalonia.UnitTests y => y.Open() == Mock.Of())); } } + + public class AppBuilder : AppBuilderBase + { + public AppBuilder() + : base(new StandardRuntimePlatform(), + builder => StandardRuntimePlatformServices.Register(builder.Instance?.GetType() + ?.GetTypeInfo().Assembly)) + { + } + + protected override bool CheckSetup => false; + } } diff --git a/tests/Avalonia.UnitTests/app.config b/tests/Avalonia.UnitTests/app.config deleted file mode 100644 index fa66e8c206..0000000000 --- a/tests/Avalonia.UnitTests/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file From 9911fff84de01873a61da067dd389d71701fe510 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 7 Jun 2018 15:11:09 +0100 Subject: [PATCH 041/149] fix carousel presenter and add unit test for condition. --- .../Presenters/CarouselPresenter.cs | 16 ++++++++- .../CarouselTests.cs | 35 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 35deea05ec..e438843078 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -125,8 +125,22 @@ namespace Avalonia.Controls.Presenters var containers = generator.Containers.ToList(); generator.Clear(); Panel.Children.RemoveAll(containers.Select(x => x.ContainerControl)); + + var newIndex = SelectedIndex; + + if(SelectedIndex < 0) + { + if(Items != null && Items.Count() > 0) + { + newIndex = 0; + } + else + { + newIndex = -1; + } + } - MoveToPage(-1, SelectedIndex >= 0 ? SelectedIndex : e.NewStartingIndex); + MoveToPage(-1, newIndex); } break; } diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index df61698209..f36e5864f6 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -170,6 +170,41 @@ namespace Avalonia.Controls.UnitTests Assert.Equal("Bar", ((TextBlock)child).Text); } + [Fact] + public void Selected_Index_Changes_To_When_Items_Assigned_Null() + { + var items = new ObservableCollection + { + "Foo", + "Bar", + "FooBar" + }; + + var target = new Carousel + { + Template = new FuncControlTemplate(CreateTemplate), + Items = items, + IsVirtualized = false + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + Assert.Single(target.GetLogicalChildren()); + + var child = target.GetLogicalChildren().Single(); + + Assert.IsType(child); + Assert.Equal("Foo", ((TextBlock)child).Text); + + target.Items = null; + + var numChildren = target.GetLogicalChildren().Count(); + + Assert.Equal(0, numChildren); + Assert.Equal(-1, target.SelectedIndex); + } + [Fact] public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex() { From 42121ec2c15c755f113edabc31da92e4307b5578 Mon Sep 17 00:00:00 2001 From: CommonGuy Date: Fri, 8 Jun 2018 08:05:28 +0200 Subject: [PATCH 042/149] Move topmost to WindowBase --- src/Avalonia.Controls/Platform/IWindowBaseImpl.cs | 5 +++++ src/Avalonia.Controls/Platform/IWindowImpl.cs | 5 ----- src/Avalonia.Controls/Window.cs | 14 -------------- src/Avalonia.Controls/WindowBase.cs | 14 ++++++++++++++ src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs | 2 ++ src/Gtk/Avalonia.Gtk3/WindowImpl.cs | 2 -- src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs | 1 + src/OSX/Avalonia.MonoMac/WindowImpl.cs | 5 ----- 8 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index 4f7ac82df7..9ba68f584e 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -72,6 +72,11 @@ namespace Avalonia.Platform /// void SetMinMaxSize(Size minSize, Size maxSize); + /// + /// Sets whether this window appears on top of all other windows + /// + void SetTopmost(bool value); + /// /// Gets platform specific display information /// diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 093e9bc57c..f1f3925133 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -55,11 +55,6 @@ namespace Avalonia.Platform /// void CanResize(bool value); - /// - /// Gets or sets whether this window appears on top of all other windows - /// - void SetTopmost(bool value); - /// /// Gets or sets a method called before the underlying implementation is destroyed. /// Return true to prevent the underlying implementation from closing. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 1943225b00..3cbfdbd657 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -104,9 +104,6 @@ namespace Avalonia.Controls public static readonly StyledProperty CanResizeProperty = AvaloniaProperty.Register(nameof(CanResize), true); - public static readonly StyledProperty TopmostProperty = - AvaloniaProperty.Register(nameof(Topmost)); - private readonly NameScope _nameScope = new NameScope(); private object _dialogResult; private readonly Size _maxPlatformClientSize; @@ -128,8 +125,6 @@ namespace Avalonia.Controls CanResizeProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.CanResize((bool)e.NewValue)); - TopmostProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetTopmost((bool)e.NewValue)); - WindowStateProperty.Changed.AddClassHandler( (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; }); } @@ -235,15 +230,6 @@ namespace Avalonia.Controls set { SetValue(CanResizeProperty, value); } } - /// - /// Gets or sets whether this window appears on top of all other windows - /// - public bool Topmost - { - get { return GetValue(TopmostProperty); } - set { SetValue(TopmostProperty, value); } - } - /// /// Gets or sets the icon of the window. /// diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index c427df1c26..5d66bee2f8 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -38,6 +38,9 @@ namespace Avalonia.Controls o => o.Owner, (o, v) => o.Owner = v); + public static readonly StyledProperty TopmostProperty = + AvaloniaProperty.Register(nameof(Topmost)); + private bool _hasExecutedInitialLayoutPass; private bool _isActive; private bool _ignoreVisibilityChange; @@ -52,6 +55,8 @@ namespace Avalonia.Controls MinHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight))); MaxWidthProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight))); MaxHeightProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue))); + + TopmostProperty.Changed.AddClassHandler((w, e) => w.PlatformImpl?.SetTopmost((bool)e.NewValue)); } public WindowBase(IWindowBaseImpl impl) : this(impl, AvaloniaLocator.Current) @@ -124,6 +129,15 @@ namespace Avalonia.Controls set { SetAndRaise(OwnerProperty, ref _owner, value); } } + /// + /// Gets or sets whether this window appears on top of all other windows + /// + public bool Topmost + { + get { return GetValue(TopmostProperty); } + set { SetValue(TopmostProperty, value); } + } + /// /// Activates the window. /// diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index 94537d3475..8a880fd306 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -416,6 +416,8 @@ namespace Avalonia.Gtk3 public void Hide() => Native.GtkWidgetHide(GtkWidget); + public void SetTopmost(bool value) => Native.GtkWindowSetKeepAbove(GtkWidget, value); + void GetGlobalPointer(out int x, out int y) { int mask; diff --git a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs index 4756d33e0f..a0b754c229 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs @@ -82,8 +82,6 @@ namespace Avalonia.Gtk3 public void CanResize(bool value) => Native.GtkWindowSetResizable(GtkWidget, value); - public void SetTopmost(bool value) => Native.GtkWindowSetKeepAbove(GtkWidget, value); - class EmptyDisposable : IDisposable { diff --git a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs index 8cbc6cbdd8..89cef59b53 100644 --- a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs +++ b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs @@ -123,6 +123,7 @@ namespace Avalonia.MonoMac public void Hide() => Window?.OrderOut(Window); + public void SetTopmost(bool value) => Window.Level = value ? NSWindowLevel.Floating : NSWindowLevel.Normal; public void BeginMoveDrag() { diff --git a/src/OSX/Avalonia.MonoMac/WindowImpl.cs b/src/OSX/Avalonia.MonoMac/WindowImpl.cs index 34a0702f8c..a2f8df6791 100644 --- a/src/OSX/Avalonia.MonoMac/WindowImpl.cs +++ b/src/OSX/Avalonia.MonoMac/WindowImpl.cs @@ -116,11 +116,6 @@ namespace Avalonia.MonoMac UpdateStyle(); } - public void SetTopmost(bool value) - { - Window.Level = value ? NSWindowLevel.Floating : NSWindowLevel.Normal; - } - public void SetTitle(string title) => Window.Title = title; class ModalDisposable : IDisposable From 835fbe251c0ddf8cd9da75ed2d377e2575e8836a Mon Sep 17 00:00:00 2001 From: CommonGuy Date: Fri, 8 Jun 2018 09:47:47 +0200 Subject: [PATCH 043/149] Implement Topmost property on Popup --- src/Avalonia.Controls/Primitives/Popup.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 656f3890cd..005717d681 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -70,6 +70,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty StaysOpenProperty = AvaloniaProperty.Register(nameof(StaysOpen), true); + /// + /// Defines the property. + /// + public static readonly StyledProperty TopmostProperty = + AvaloniaProperty.Register(nameof(Topmost)); + private bool _isOpen; private PopupRoot _popupRoot; private TopLevel _topLevel; @@ -84,6 +90,7 @@ namespace Avalonia.Controls.Primitives IsHitTestVisibleProperty.OverrideDefaultValue(false); ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); IsOpenProperty.Changed.AddClassHandler(x => x.IsOpenChanged); + TopmostProperty.Changed.AddClassHandler((p, e) => p.PopupRoot.Topmost = (bool)e.NewValue); } /// @@ -194,6 +201,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(StaysOpenProperty, value); } } + /// + /// Gets or sets whether this popup appears on top of all other windows + /// + public bool Topmost + { + get { return GetValue(TopmostProperty); } + set { SetValue(TopmostProperty, value); } + } + /// /// Gets the root of the popup window. /// From c29a21afc2eee3fbdb04cab379360f7ce08fd401 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 10 Jun 2018 11:39:52 +0100 Subject: [PATCH 044/149] ensure tooltips fit inside screen edges. --- src/Avalonia.Controls/ToolTip.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 464e4188d2..10e964d014 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -234,11 +234,12 @@ namespace Avalonia.Controls { Close(); - _popup = new PopupRoot { Content = this }; + _popup = new PopupRoot { Content = this, }; ((ISetLogicalParent)_popup).SetParent(control); _popup.Position = Popup.GetPosition(control, GetPlacement(control), _popup, GetHorizontalOffset(control), GetVerticalOffset(control)); _popup.Show(); + _popup.SnapInsideScreenEdges(); } private void Close() From 61816fdf92c90b2929d3e48dd4e8bd5808d18654 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 11 Jun 2018 14:06:48 +0200 Subject: [PATCH 045/149] Relative move command fix --- src/Avalonia.Visuals/Media/PathMarkupParser.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 0307701e82..c3fcf19148 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -232,7 +232,9 @@ namespace Avalonia.Media private void AddMove(CommandToken commandToken) { - var currentPoint = commandToken.ReadPoint(); + var currentPoint = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); _currentPoint = currentPoint; From 1a4fd00f2b77d0a648b00ca680227bb164c53ddd Mon Sep 17 00:00:00 2001 From: Ivan Garcia Date: Tue, 12 Jun 2018 17:25:43 -0400 Subject: [PATCH 046/149] Fix trailing newlines in textboxes when using NetCore/Skia Fixes issue where if a textbox's text ended with a newline character the additional line at the end would not render. Did this by, in these cases, adding an additional blank AvaloniaFormattedTextLine. This also fixes an issue where line widths were being measured incorrectly because the Substring() call used for that measurement was using line.Start before it was initialized for the current line. --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 31 +++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 00f0a48a7b..13dcd9669d 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -560,13 +560,11 @@ namespace Avalonia.Skia } measured = LineBreak(Text, curOff, length, _paint, constraint, out trailingnumber); - AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine(); line.TextLength = measured; - + line.Start = curOff; subString = Text.Substring(line.Start, line.TextLength); lineWidth = _paint.MeasureText(subString); - line.Start = curOff; line.Length = measured - trailingnumber; line.Width = lineWidth; line.Height = _lineHeight; @@ -575,10 +573,33 @@ namespace Avalonia.Skia _skiaLines.Add(line); curY += _lineHeight; - curY += mLeading; - curOff += measured; + + //if this is the last line and there are trailing newline characters then + //insert a additional line + if (curOff >= length) + { + var subStringMinusNewlines = subString.TrimEnd('\n', '\r'); + var lengthDiff = subString.Length - subStringMinusNewlines.Length; + if (lengthDiff > 0) + { + AvaloniaFormattedTextLine lastLine = new AvaloniaFormattedTextLine(); + lastLine.TextLength = lengthDiff; + lastLine.Start = curOff - lengthDiff; + var lastLineSubString = Text.Substring(line.Start, line.TextLength); + var lastLineWidth = _paint.MeasureText(lastLineSubString); + lastLine.Length = 0; + lastLine.Width = lastLineWidth; + lastLine.Height = _lineHeight; + lastLine.Top = curY; + + _skiaLines.Add(lastLine); + + curY += _lineHeight; + curY += mLeading; + } + } } // Now convert to Avalonia data formats From 3d092b9fcf31bdb7e7132565b0bd9b2088018149 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Tue, 12 Jun 2018 23:41:13 -0400 Subject: [PATCH 047/149] Added a Scroll event to ScrollBar The event is fired when the ScrollBar's value is changed through interaction with the ScrollBar's Track --- src/Avalonia.Controls/Primitives/ScrollBar.cs | 37 +++++++++++++ .../Primitives/ScrollEventType.cs | 53 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/Avalonia.Controls/Primitives/ScrollEventType.cs diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 0057b15150..3ddcb06303 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -6,9 +6,21 @@ using System.Reactive; using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Interactivity; +using Avalonia.Input; namespace Avalonia.Controls.Primitives { + public class ScrollEventArgs : EventArgs + { + public ScrollEventArgs(ScrollEventType eventType, double newValue) + { + ScrollEventType = eventType; + NewValue = newValue; + } + public double NewValue { get; private set; } + public ScrollEventType ScrollEventType { get; private set; } + } + /// /// A scrollbar control. /// @@ -44,6 +56,9 @@ namespace Avalonia.Controls.Primitives { PseudoClass(OrientationProperty, o => o == Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal"); + + Thumb.DragDeltaEvent.AddClassHandler(o => o.OnThumbDragDelta, RoutingStrategies.Bubble); + Thumb.DragCompletedEvent.AddClassHandler(o => o.OnThumbDragComplete, RoutingStrategies.Bubble); } /// @@ -88,6 +103,8 @@ namespace Avalonia.Controls.Primitives set { SetValue(OrientationProperty, value); } } + public event EventHandler Scroll; + /// /// Calculates whether the scrollbar should be visible. /// @@ -140,6 +157,8 @@ namespace Avalonia.Controls.Primitives _pageUpButton = e.NameScope.Find