From add676efab0f904b6200824c567ac6d238462419 Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 13 Jul 2023 13:08:47 +0300 Subject: [PATCH 01/57] Add failing tests --- .../TransitioningContentControlTests.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs index 02dd4e6c03..0e817b1887 100644 --- a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs @@ -183,6 +183,31 @@ namespace Avalonia.Controls.UnitTests Assert.Equal("bar", presenter2.Content); } + [Fact] + public void Logical_Children_Dont_Duplicated() + { + using var app = Start(); + var (target, transition) = CreateTarget(""); + + var childControl = new Control(); + target.Content = childControl; + + // There should be two, One the initial content and one the new content. + Assert.Equal(2, target.LogicalChildren.Count); + } + + [Fact] + public void First_Presenter_Register_TCC_As_Host() + { + using var app = Start(); + var (target, transition) = CreateTarget(""); + + var childControl = new Control(); + target.Presenter!.Content = childControl; + + Assert.Contains(childControl, target.LogicalChildren); + } + private static IDisposable Start() { return UnitTestApplication.Start( From f7a584e86e0db552cd5509bd827361360ff8211c Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 13 Jul 2023 13:14:11 +0300 Subject: [PATCH 02/57] Return true when first presenter register and fix logical children duplicated --- src/Avalonia.Controls/ContentControl.cs | 16 ++++++++++++++++ .../TransitioningContentControl.cs | 11 +++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 867dca2be1..176157001e 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -95,6 +95,17 @@ namespace Avalonia.Controls /// IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + /// + /// Determine whether manage his LogicalChildren + ///(default behavior) himself, or leaves the management to the inherited control. + /// + /// + /// The default value is false, So the manages itself, + /// if you want to bypass this behavior and manage LogicalChildren yourself, set + /// the to true. + /// + protected virtual bool BypassLogicalChildrenManangment => false; + /// bool IContentPresenterHost.RegisterContentPresenter(ContentPresenter presenter) { @@ -118,6 +129,11 @@ namespace Avalonia.Controls private void ContentChanged(AvaloniaPropertyChangedEventArgs e) { + if (BypassLogicalChildrenManangment) + { + return; + } + if (e.OldValue is ILogical oldChild) { LogicalChildren.Remove(oldChild); diff --git a/src/Avalonia.Controls/TransitioningContentControl.cs b/src/Avalonia.Controls/TransitioningContentControl.cs index bf540698f1..75f95d1d68 100644 --- a/src/Avalonia.Controls/TransitioningContentControl.cs +++ b/src/Avalonia.Controls/TransitioningContentControl.cs @@ -36,6 +36,9 @@ public class TransitioningContentControl : ContentControl set => SetValue(PageTransitionProperty, value); } + /// + protected override bool BypassLogicalChildrenManangment => true; + protected override Size ArrangeOverride(Size finalSize) { var result = base.ArrangeOverride(finalSize); @@ -79,8 +82,12 @@ public class TransitioningContentControl : ContentControl protected override bool RegisterContentPresenter(ContentPresenter presenter) { - if (!base.RegisterContentPresenter(presenter) && - presenter is ContentPresenter p && + if (base.RegisterContentPresenter(presenter)) + { + return true; + } + + if (presenter is ContentPresenter p && p.Name == "PART_ContentPresenter2") { _presenter2 = p; From abc7eea3369642162617c9a20a91b27e0d7b87dc Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 13 Jul 2023 15:33:42 +0300 Subject: [PATCH 03/57] simplifies the first test --- .../TransitioningContentControlTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs index 0e817b1887..210b24b3ad 100644 --- a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs @@ -188,12 +188,12 @@ namespace Avalonia.Controls.UnitTests { using var app = Start(); var (target, transition) = CreateTarget(""); + target.PageTransition = null; var childControl = new Control(); target.Content = childControl; - // There should be two, One the initial content and one the new content. - Assert.Equal(2, target.LogicalChildren.Count); + Assert.Equal(1, target.LogicalChildren.Count); } [Fact] From 35be25ee2e99f03ca0dfa6f96f0c6dd47edf8d05 Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 13 Jul 2023 16:38:11 +0300 Subject: [PATCH 04/57] typo --- src/Avalonia.Controls/ContentControl.cs | 4 ++-- src/Avalonia.Controls/TransitioningContentControl.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 176157001e..362931eafa 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -102,9 +102,9 @@ namespace Avalonia.Controls /// /// The default value is false, So the manages itself, /// if you want to bypass this behavior and manage LogicalChildren yourself, set - /// the to true. + /// the to true. /// - protected virtual bool BypassLogicalChildrenManangment => false; + protected virtual bool BypassLogicalChildrenManagement => false; /// bool IContentPresenterHost.RegisterContentPresenter(ContentPresenter presenter) diff --git a/src/Avalonia.Controls/TransitioningContentControl.cs b/src/Avalonia.Controls/TransitioningContentControl.cs index 75f95d1d68..c113afde17 100644 --- a/src/Avalonia.Controls/TransitioningContentControl.cs +++ b/src/Avalonia.Controls/TransitioningContentControl.cs @@ -37,7 +37,7 @@ public class TransitioningContentControl : ContentControl } /// - protected override bool BypassLogicalChildrenManangment => true; + protected override bool BypassLogicalChildrenManagement => true; protected override Size ArrangeOverride(Size finalSize) { From 007800357865a37db308512ff4731b42ee9bcf15 Mon Sep 17 00:00:00 2001 From: daniel Date: Thu, 13 Jul 2023 21:02:45 +0300 Subject: [PATCH 05/57] typo --- src/Avalonia.Controls/ContentControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 362931eafa..47f7598900 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -129,7 +129,7 @@ namespace Avalonia.Controls private void ContentChanged(AvaloniaPropertyChangedEventArgs e) { - if (BypassLogicalChildrenManangment) + if (BypassLogicalChildrenManagement) { return; } From 0e7b8f6f4589195ae35e933c64f89f9478832fb1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jul 2023 23:17:48 +0200 Subject: [PATCH 06/57] Allow embedded root automation peers. --- .../Avalonia.Win32/Automation/RootAutomationNode.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index 739f0ac251..0a73c8bc7b 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -33,7 +33,7 @@ namespace Avalonia.Win32.Automation return null; var p = WindowImpl.PointToClient(new PixelPoint((int)x, (int)y)); - var found = InvokeSync(() => Peer.GetPeerFromPoint(p)); + var found = InvokeSync(() => GetPeerFromPoint(p)); var result = GetOrCreate(found) as IRawElementProviderFragment; return result; } @@ -101,5 +101,15 @@ namespace Avalonia.Win32.Automation return result; } } + + private AutomationPeer? GetPeerFromPoint(Point p) + { + var hit = Peer.GetPeerFromPoint(p); + + while (hit != Peer && hit?.GetProvider() is { } embeddedRoot) + hit = embeddedRoot.GetPeerFromPoint(p); + + return hit; + } } } From 2c91d7f89323c736dd4afeaf6e13ad5efc247b06 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jul 2023 12:45:58 +0200 Subject: [PATCH 07/57] Handle null from RootProvider_GetWindow. --- native/Avalonia.Native/src/OSX/automation.mm | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index d0c8d7a9db..4b325a092d 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -73,6 +73,13 @@ private: if (peer->IsRootProvider()) { auto window = peer->RootProvider_GetWindow(); + + if (window == nullptr) + { + NSLog(@"IRootProvider.PlatformImpl returned null or a non-WindowBaseImpl."); + return nil; + } + auto holder = dynamic_cast(window); auto view = holder->GetNSView(); return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view]; From 8d5ef676f5098257889925419501799596aa1bf7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jul 2023 12:46:25 +0200 Subject: [PATCH 08/57] Allow non-ControlAutomationPeer IRootProviders. --- src/Avalonia.Native/AvnAutomationPeer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 6c4e96b31b..038b62a7f6 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -74,10 +74,11 @@ namespace Avalonia.Native public int IsRootProvider() => (_inner is IRootProvider).AsComBool(); - public IAvnWindowBase RootProvider_GetWindow() + public IAvnWindowBase? RootProvider_GetWindow() { - var window = (WindowBase)((ControlAutomationPeer)_inner).Owner; - return ((WindowBaseImpl)window.PlatformImpl!).Native; + if (((IRootProvider)_inner).PlatformImpl is WindowBaseImpl impl) + return impl.Native; + return null; } public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus()); From da993425e7a18c545d591259eb1a19a8bc27f5a9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jul 2023 13:46:35 +0200 Subject: [PATCH 09/57] Fix determining if a peer supports a provider. We should use `GetProvider` instead of a plain cast as a peer may decide to dynamically support a provider, or delegate its implementation. --- src/Avalonia.Native/AvnAutomationPeer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 038b62a7f6..6b1b4a7a03 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -72,7 +72,7 @@ namespace Avalonia.Native Node = node; } - public int IsRootProvider() => (_inner is IRootProvider).AsComBool(); + public int IsRootProvider() => (_inner.GetProvider() is not null).AsComBool(); public IAvnWindowBase? RootProvider_GetWindow() { @@ -104,7 +104,7 @@ namespace Avalonia.Native return Wrap(result); } - public int IsExpandCollapseProvider() => (_inner is IExpandCollapseProvider).AsComBool(); + public int IsExpandCollapseProvider() => (_inner.GetProvider() is not null).AsComBool(); public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch { @@ -128,14 +128,14 @@ namespace Avalonia.Native public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange; public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value); - public int IsSelectionItemProvider() => (_inner is ISelectionItemProvider).AsComBool(); + public int IsSelectionItemProvider() => (_inner.GetProvider() is not null).AsComBool(); public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool(); - public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool(); + public int IsToggleProvider() => (_inner.GetProvider() is not null).AsComBool(); public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState; public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle(); - public int IsValueProvider() => (_inner is IValueProvider).AsComBool(); + public int IsValueProvider() => (_inner.GetProvider() is not null).AsComBool(); public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString(); public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value); From c9dfda42ebd524e19205d756ba6c625496e9361d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jul 2023 17:31:43 +0200 Subject: [PATCH 10/57] More fixing of provider resolution. The previous commit missed some providers, and we also need to call `GetProvider` when calling members on the provider. --- src/Avalonia.Native/AvnAutomationPeer.cs | 88 +++++++++++++----------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 6b1b4a7a03..8069ac76bf 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -48,6 +48,13 @@ namespace Avalonia.Native public void SetFocus() => _inner.SetFocus(); public int ShowContextMenu() => _inner.ShowContextMenu().AsComBool(); + public void SetNode(IAvnAutomationNode node) + { + if (Node is not null) + throw new InvalidOperationException("The AvnAutomationPeer already has a node."); + Node = node; + } + public IAvnAutomationPeer? RootPeer { get @@ -65,27 +72,22 @@ namespace Avalonia.Native } } - public void SetNode(IAvnAutomationNode node) - { - if (Node is not null) - throw new InvalidOperationException("The AvnAutomationPeer already has a node."); - Node = node; - } - - public int IsRootProvider() => (_inner.GetProvider() is not null).AsComBool(); + private IRootProvider RootProvider => GetProvider(); + private IExpandCollapseProvider ExpandCollapseProvider => GetProvider(); + private IInvokeProvider InvokeProvider => GetProvider(); + private IRangeValueProvider RangeValueProvider => GetProvider(); + private ISelectionItemProvider SelectionItemProvider => GetProvider(); + private IToggleProvider ToggleProvider => GetProvider(); + private IValueProvider ValueProvider => GetProvider(); - public IAvnWindowBase? RootProvider_GetWindow() - { - if (((IRootProvider)_inner).PlatformImpl is WindowBaseImpl impl) - return impl.Native; - return null; - } - - public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus()); + public int IsRootProvider() => IsProvider(); + + public IAvnWindowBase? RootProvider_GetWindow() => (RootProvider.PlatformImpl as WindowBaseImpl)?.Native; + public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(RootProvider.GetFocus()); public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point) { - var result = ((IRootProvider)_inner).GetPeerFromPoint(point.ToAvaloniaPoint()); + var result = RootProvider.GetPeerFromPoint(point.ToAvaloniaPoint()); if (result is null) return null; @@ -104,46 +106,54 @@ namespace Avalonia.Native return Wrap(result); } - public int IsExpandCollapseProvider() => (_inner.GetProvider() is not null).AsComBool(); + public int IsExpandCollapseProvider() => IsProvider(); - public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch + public int ExpandCollapseProvider_GetIsExpanded() => ExpandCollapseProvider.ExpandCollapseState switch { ExpandCollapseState.Expanded => 1, ExpandCollapseState.PartiallyExpanded => 1, _ => 0, }; - public int ExpandCollapseProvider_GetShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool(); - public void ExpandCollapseProvider_Expand() => ((IExpandCollapseProvider)_inner).Expand(); - public void ExpandCollapseProvider_Collapse() => ((IExpandCollapseProvider)_inner).Collapse(); + public int ExpandCollapseProvider_GetShowsMenu() => ExpandCollapseProvider.ShowsMenu.AsComBool(); + public void ExpandCollapseProvider_Expand() => ExpandCollapseProvider.Expand(); + public void ExpandCollapseProvider_Collapse() => ExpandCollapseProvider.Collapse(); - public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool(); - public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke(); + public int IsInvokeProvider() => IsProvider(); + public void InvokeProvider_Invoke() => InvokeProvider.Invoke(); - public int IsRangeValueProvider() => (_inner is IRangeValueProvider).AsComBool(); - public double RangeValueProvider_GetValue() => ((IRangeValueProvider)_inner).Value; - public double RangeValueProvider_GetMinimum() => ((IRangeValueProvider)_inner).Minimum; - public double RangeValueProvider_GetMaximum() => ((IRangeValueProvider)_inner).Maximum; - public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange; - public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange; - public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value); + public int IsRangeValueProvider() => IsProvider(); + public double RangeValueProvider_GetValue() => RangeValueProvider.Value; + public double RangeValueProvider_GetMinimum() => RangeValueProvider.Minimum; + public double RangeValueProvider_GetMaximum() => RangeValueProvider.Maximum; + public double RangeValueProvider_GetSmallChange() => RangeValueProvider.SmallChange; + public double RangeValueProvider_GetLargeChange() => RangeValueProvider.LargeChange; + public void RangeValueProvider_SetValue(double value) => RangeValueProvider.SetValue(value); - public int IsSelectionItemProvider() => (_inner.GetProvider() is not null).AsComBool(); - public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool(); + public int IsSelectionItemProvider() => IsProvider(); + public int SelectionItemProvider_IsSelected() => SelectionItemProvider.IsSelected.AsComBool(); - public int IsToggleProvider() => (_inner.GetProvider() is not null).AsComBool(); - public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState; - public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle(); + public int IsToggleProvider() => IsProvider(); + public int ToggleProvider_GetToggleState() => (int)ToggleProvider.ToggleState; + public void ToggleProvider_Toggle() => ToggleProvider.Toggle(); - public int IsValueProvider() => (_inner.GetProvider() is not null).AsComBool(); - public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString(); - public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value); + public int IsValueProvider() => IsProvider(); + public IAvnString ValueProvider_GetValue() => ValueProvider.Value.ToAvnString(); + public void ValueProvider_SetValue(string value) => ValueProvider.SetValue(value); [return: NotNullIfNotNull("peer")] public static AvnAutomationPeer? Wrap(AutomationPeer? peer) { return peer is null ? null : s_wrappers.GetValue(peer, x => new(peer)); } + + private T GetProvider() + { + return _inner.GetProvider() ?? throw new InvalidOperationException( + $"The peer {_inner} does not implement {typeof(T)}."); + } + + private int IsProvider() => (_inner.GetProvider() is not null).AsComBool(); } internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray From 6b68a8e5c6480edb095c33d0fd744e01ef2c4637 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jul 2023 19:09:33 +0200 Subject: [PATCH 11/57] More fixing of provider resolution. Found a few more places that were doing casts instead of calling `GetProvider()`. --- .../Automation/Peers/ItemsControlAutomationPeer.cs | 2 +- .../Automation/Peers/ListItemAutomationPeer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs index db16bf0a53..64727c43c5 100644 --- a/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs @@ -28,7 +28,7 @@ namespace Avalonia.Automation.Peers if (!_searchedForScrollable) { if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable) - _scroller = GetOrCreate(scrollable) as IScrollProvider; + _scroller = GetOrCreate(scrollable).GetProvider(); _searchedForScrollable = true; } diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs index aea91b5e26..dab8c45567 100644 --- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -22,7 +22,7 @@ namespace Avalonia.Automation.Peers if (Owner.Parent is Control parent) { var parentPeer = GetOrCreate(parent); - return parentPeer as ISelectionProvider; + return parentPeer.GetProvider(); } return null; From 8ffbb2a214150830c278aff0c60d45d196594548 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jul 2023 19:52:36 +0200 Subject: [PATCH 12/57] More fixing of provider resolution. Arrgh! Forgot to save the file. --- src/Avalonia.Native/AvnAutomationPeer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 8069ac76bf..76cae2684f 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -62,7 +62,7 @@ namespace Avalonia.Native var peer = _inner; var parent = peer.GetParent(); - while (peer is not IRootProvider && parent is not null) + while (peer.GetProvider() is null && parent is not null) { peer = parent; parent = peer.GetParent(); From 02789d2d48e442997d70a5eba024d02094fcb554 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jul 2023 00:19:37 +0200 Subject: [PATCH 13/57] Revert "Allow embedded root automation peers." This reverts commit 0e7b8f6f4589195ae35e933c64f89f9478832fb1. The code is in the wrong place. --- .../Avalonia.Win32/Automation/RootAutomationNode.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index 0a73c8bc7b..739f0ac251 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -33,7 +33,7 @@ namespace Avalonia.Win32.Automation return null; var p = WindowImpl.PointToClient(new PixelPoint((int)x, (int)y)); - var found = InvokeSync(() => GetPeerFromPoint(p)); + var found = InvokeSync(() => Peer.GetPeerFromPoint(p)); var result = GetOrCreate(found) as IRawElementProviderFragment; return result; } @@ -101,15 +101,5 @@ namespace Avalonia.Win32.Automation return result; } } - - private AutomationPeer? GetPeerFromPoint(Point p) - { - var hit = Peer.GetPeerFromPoint(p); - - while (hit != Peer && hit?.GetProvider() is { } embeddedRoot) - hit = embeddedRoot.GetPeerFromPoint(p); - - return hit; - } } } From 0c7c315a10c114427e7a6514bf37949aeaf34122 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jul 2023 00:23:19 +0200 Subject: [PATCH 14/57] Hit-test embedded root automation peers. --- .../Peers/WindowBaseAutomationPeer.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs index 9ec65592fa..3786ba32c7 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Globalization; using Avalonia.Automation.Provider; using Avalonia.Controls; using Avalonia.Input; @@ -32,7 +33,21 @@ namespace Avalonia.Automation.Peers public AutomationPeer? GetPeerFromPoint(Point p) { var hit = Owner.GetVisualAt(p)?.FindAncestorOfType(includeSelf: true); - return hit is object ? GetOrCreate(hit) : null; + + if (hit is null) + return null; + + var peer = GetOrCreate(hit); + + while (peer != this && peer.GetProvider() is { } embedded) + { + var embeddedHit = embedded.GetPeerFromPoint(p); + if (embeddedHit is null) + break; + peer = embeddedHit; + } + + return peer; } protected void StartTrackingFocus() From b7fcb141420001364d7a06efced40dfe2d357e93 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jul 2023 12:53:44 +0200 Subject: [PATCH 15/57] Added IEmbeddedRootProvider. For some reason, on win32 embedded `IRawElementProviderFragmentRoot`s just don't show up, so we need an interface to distinguish between "actual" root peers and "embedded" root peers. Ideally `IRootProvider` and `IEmbeddedRootProvider` would share a common interface but that would be a breaking change. --- .../Peers/WindowBaseAutomationPeer.cs | 2 +- .../Provider/IEmbeddedRootProvider.cs | 33 +++++++++ .../Automation/Provider/IRootProvider.cs | 25 +++++++ .../Automation/AutomationNode.cs | 69 +++++++++++-------- .../Automation/RootAutomationNode.cs | 48 ++----------- 5 files changed, 108 insertions(+), 69 deletions(-) create mode 100644 src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs diff --git a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs index 3786ba32c7..ceb695422d 100644 --- a/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs @@ -39,7 +39,7 @@ namespace Avalonia.Automation.Peers var peer = GetOrCreate(hit); - while (peer != this && peer.GetProvider() is { } embedded) + while (peer != this && peer.GetProvider() is { } embedded) { var embeddedHit = embedded.GetPeerFromPoint(p); if (embeddedHit is null) diff --git a/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs new file mode 100644 index 0000000000..1b1caef182 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs @@ -0,0 +1,33 @@ +using System; +using Avalonia.Automation.Peers; + +namespace Avalonia.Automation.Provider +{ + /// + /// Exposure methods and properties to support UI Automation client access to the root of an + /// automation tree hosted by another UI framework. + /// + /// + /// This interface is implemented by the class, and can be used + /// to embed an automation tree from a 3rd party UI framework that wishes to use Avalonia's + /// automation support. + /// + public interface IEmbeddedRootProvider + { + /// + /// Gets the currently focused element. + /// + AutomationPeer? GetFocus(); + + /// + /// Gets the element at the specified point, expressed in top-level coordinates. + /// + /// The point. + AutomationPeer? GetPeerFromPoint(Point p); + + /// + /// Raised by the automation peer when the focus changes. + /// + event EventHandler? FocusChanged; + } +} diff --git a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs index ce38059559..6a266da5c5 100644 --- a/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs +++ b/src/Avalonia.Controls/Automation/Provider/IRootProvider.cs @@ -4,11 +4,36 @@ using Avalonia.Platform; namespace Avalonia.Automation.Provider { + /// + /// Exposes methods and properties to support UI Automation client access to the root of an + /// automation tree. + /// + /// + /// This interface is implemented by the class, and should only + /// be implemented on true root elements, such as Windows. To embed an automation tree, use + /// instead. + /// public interface IRootProvider { + /// + /// Gets the platform implementation of the TopLevel for the element. + /// ITopLevelImpl? PlatformImpl { get; } + + /// + /// Gets the currently focused element. + /// AutomationPeer? GetFocus(); + + /// + /// Gets the element at the specified point, expressed in top-level coordinates. + /// + /// The point. AutomationPeer? GetPeerFromPoint(Point p); + + /// + /// Raised by the automation peer when the focus changes. + /// event EventHandler? FocusChanged; } } diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 3eeedc4b5d..e835c6a57a 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -54,25 +54,11 @@ namespace Avalonia.Win32.Automation _runtimeId = new int[] { 3, GetHashCode() }; Peer = peer; s_nodes.Add(peer, this); - peer.ChildrenChanged += Peer_ChildrenChanged; - peer.PropertyChanged += Peer_PropertyChanged; - } - - private void Peer_ChildrenChanged(object? sender, EventArgs e) - { - ChildrenChanged(); - } + peer.ChildrenChanged += OnPeerChildrenChanged; + peer.PropertyChanged += OnPeerPropertyChanged; - private void Peer_PropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) - { - if (s_propertyMap.TryGetValue(e.Property, out var id)) - { - UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent( - this, - (int)id, - e.OldValue as IConvertible, - e.NewValue as IConvertible); - } + if (Peer.GetProvider() is { } embeddedRoot) + embeddedRoot.FocusChanged += OnEmbeddedRootFocusChanged; } public AutomationPeer Peer { get; protected set; } @@ -95,15 +81,6 @@ namespace Avalonia.Win32.Automation public virtual IRawElementProviderSimple? HostRawElementProvider => null; public ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider; - public void ChildrenChanged() - { - UiaCoreProviderApi.UiaRaiseStructureChangedEvent( - this, - StructureChangeType.ChildrenInvalidated, - null, - 0); - } - [return: MarshalAs(UnmanagedType.IUnknown)] public virtual object? GetPatternProvider(int patternId) { @@ -250,6 +227,21 @@ namespace Avalonia.Win32.Automation throw new NotSupportedException(); } + protected void RaiseChildrenChanged() + { + UiaCoreProviderApi.UiaRaiseStructureChangedEvent( + this, + StructureChangeType.ChildrenInvalidated, + null, + 0); + } + + protected void RaiseFocusChanged(AutomationNode? focused) + { + UiaCoreProviderApi.UiaRaiseAutomationEvent( + focused, + (int)UiaEventId.AutomationFocusChanged); + } private AutomationNode? GetRoot() { @@ -267,6 +259,29 @@ namespace Avalonia.Win32.Automation return peer is object ? GetOrCreate(peer) : null; } + private void OnPeerChildrenChanged(object? sender, EventArgs e) + { + RaiseChildrenChanged(); + } + + private void OnPeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) + { + if (s_propertyMap.TryGetValue(e.Property, out var id)) + { + UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent( + this, + (int)id, + e.OldValue as IConvertible, + e.NewValue as IConvertible); + } + } + + private void OnEmbeddedRootFocusChanged(object? sender, EventArgs e) + { + if (Peer.GetProvider() is { } embeddedRoot) + RaiseFocusChanged(GetOrCreate(embeddedRoot.GetFocus())); + } + private static AutomationNode Create(AutomationPeer peer) { return peer.GetProvider() is object ? diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index 739f0ac251..7334186c80 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -9,18 +9,14 @@ using Avalonia.Win32.Interop.Automation; namespace Avalonia.Win32.Automation { [RequiresUnreferencedCode("Requires .NET COM interop")] - internal class RootAutomationNode : AutomationNode, - IRawElementProviderFragmentRoot, - IRawElementProviderAdviseEvents + internal class RootAutomationNode : AutomationNode, IRawElementProviderFragmentRoot { - private int _raiseFocusChanged; - public RootAutomationNode(AutomationPeer peer) : base(peer) { Peer = base.Peer.GetProvider() ?? throw new AvaloniaInternalException( "Attempt to create RootAutomationNode from peer which does not implement IRootProvider."); - Peer.FocusChanged += FocusChanged; + Peer.FocusChanged += OnRootFocusChanged; } public override IRawElementProviderFragmentRoot? FragmentRoot => this; @@ -44,41 +40,6 @@ namespace Avalonia.Win32.Automation return GetOrCreate(focus); } - void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties) - { - switch ((UiaEventId)eventId) - { - case UiaEventId.AutomationFocusChanged: - ++_raiseFocusChanged; - break; - } - } - - void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties) - { - switch ((UiaEventId)eventId) - { - case UiaEventId.AutomationFocusChanged: - --_raiseFocusChanged; - break; - } - } - - protected void RaiseFocusChanged(AutomationNode? focused) - { - if (_raiseFocusChanged > 0) - { - UiaCoreProviderApi.UiaRaiseAutomationEvent( - focused, - (int)UiaEventId.AutomationFocusChanged); - } - } - - public void FocusChanged(object? sender, EventArgs e) - { - RaiseFocusChanged(GetOrCreate(Peer.GetFocus())); - } - public Rect ToScreen(Rect rect) { if (WindowImpl is null) @@ -101,5 +62,10 @@ namespace Avalonia.Win32.Automation return result; } } + + private void OnRootFocusChanged(object? sender, EventArgs e) + { + RaiseFocusChanged(GetOrCreate(Peer.GetFocus())); + } } } From 4fc2a0dfe799c7d944ad7bd114185cca362cd7a6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jul 2023 17:04:45 +0200 Subject: [PATCH 16/57] Add IEmbeddedRootProvider to AvnAutomationPeer. --- src/Avalonia.Native/AvnAutomationPeer.cs | 29 +++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 76cae2684f..5933dc6c92 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -72,10 +72,11 @@ namespace Avalonia.Native } } - private IRootProvider RootProvider => GetProvider(); + private IEmbeddedRootProvider EmbeddedRootProvider => GetProvider(); private IExpandCollapseProvider ExpandCollapseProvider => GetProvider(); private IInvokeProvider InvokeProvider => GetProvider(); private IRangeValueProvider RangeValueProvider => GetProvider(); + private IRootProvider RootProvider => GetProvider(); private ISelectionItemProvider SelectionItemProvider => GetProvider(); private IToggleProvider ToggleProvider => GetProvider(); private IValueProvider ValueProvider => GetProvider(); @@ -106,6 +107,32 @@ namespace Avalonia.Native return Wrap(result); } + + public int IsEmbeddedRootProvider() => IsProvider(); + + public IAvnAutomationPeer? EmbeddedRootProvider_GetFocus() => Wrap(EmbeddedRootProvider.GetFocus()); + + public IAvnAutomationPeer? EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point) + { + var result = EmbeddedRootProvider.GetPeerFromPoint(point.ToAvaloniaPoint()); + + if (result is null) + return null; + + // The OSX accessibility APIs expect non-ignored elements when hit-testing. + while (!result.IsControlElement()) + { + var parent = result.GetParent(); + + if (parent is not null) + result = parent; + else + break; + } + + return Wrap(result); + } + public int IsExpandCollapseProvider() => IsProvider(); public int ExpandCollapseProvider_GetIsExpanded() => ExpandCollapseProvider.ExpandCollapseState switch From c1645ca31ae595a1bed83413aba59020d011d24c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jul 2023 19:37:37 +0200 Subject: [PATCH 17/57] Allow an AutomationPeer to override its visual root. This is needed for example when a UI framework hosts a peer in the automation tree of a main window whose control is actually hosted in a popup. It allows the bounding rectangle to be calculated correctly in that case. s --- .../Automation/Peers/AutomationPeer.cs | 23 ++++++++++++++++++- .../Automation/AutomationNode.cs | 16 +++---------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index 3d3fe35d29..a264909ba6 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -115,9 +115,14 @@ namespace Avalonia.Automation.Peers /// /// Gets the that is the parent of this . /// - /// public AutomationPeer? GetParent() => GetParentCore(); + /// + /// Gets the that is the root of this 's + /// visual tree. + /// + public AutomationPeer? GetVisualRoot() => GetVisualRootCore(); + /// /// Gets a value that indicates whether the element that is associated with this automation /// peer currently has keyboard focus. @@ -247,6 +252,22 @@ namespace Avalonia.Automation.Peers return GetAutomationControlTypeCore(); } + protected virtual AutomationPeer? GetVisualRootCore() + { + var parent = GetParent(); + + while (parent != null) + { + var nextParent = parent.GetParent(); + if (nextParent == null) + return parent; + parent = nextParent; + } + + return null; + } + + protected virtual bool IsContentElementOverrideCore() { return IsControlElement() && IsContentElementCore(); diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index e835c6a57a..569f7da738 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -75,7 +75,7 @@ namespace Avalonia.Win32.Automation public virtual IRawElementProviderFragmentRoot? FragmentRoot { - get => InvokeSync(() => GetRoot()) as IRawElementProviderFragmentRoot; + get => InvokeSync(() => GetRoot()); } public virtual IRawElementProviderSimple? HostRawElementProvider => null; @@ -243,20 +243,10 @@ namespace Avalonia.Win32.Automation (int)UiaEventId.AutomationFocusChanged); } - private AutomationNode? GetRoot() + private RootAutomationNode? GetRoot() { Dispatcher.UIThread.VerifyAccess(); - - var peer = Peer; - var parent = peer.GetParent(); - - while (peer.GetProvider() is null && parent is object) - { - peer = parent; - parent = peer.GetParent(); - } - - return peer is object ? GetOrCreate(peer) : null; + return GetOrCreate(Peer.GetVisualRoot()) as RootAutomationNode; } private void OnPeerChildrenChanged(object? sender, EventArgs e) From 651f558b67578971a981cdcba479ecb5783cd74b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jul 2023 19:58:16 +0200 Subject: [PATCH 18/57] Added new members to IAvnAutomationPeer. --- src/Avalonia.Native/AvnAutomationPeer.cs | 1 + src/Avalonia.Native/avn.idl | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index 5933dc6c92..d2d93b69a9 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -39,6 +39,7 @@ namespace Avalonia.Native public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy()); public IAvnString Name => _inner.GetName().ToAvnString(); public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent()); + public IAvnAutomationPeer? VisualRoot => Wrap(_inner.GetVisualRoot()); public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool(); public int IsContentElement() => _inner.IsContentElement().AsComBool(); diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index bc372bbcb5..0911e5ffff 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -921,6 +921,7 @@ interface IAvnAutomationPeer : IUnknown IAvnAutomationPeer* GetLabeledBy(); IAvnString* GetName(); IAvnAutomationPeer* GetParent(); + IAvnAutomationPeer* GetVisualRoot(); bool HasKeyboardFocus(); bool IsContentElement(); bool IsControlElement(); @@ -935,7 +936,11 @@ interface IAvnAutomationPeer : IUnknown IAvnWindowBase* RootProvider_GetWindow(); IAvnAutomationPeer* RootProvider_GetFocus(); IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point); - + + bool IsEmbeddedRootProvider(); + IAvnAutomationPeer* EmbeddedRootProvider_GetFocus(); + IAvnAutomationPeer* EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point); + bool IsExpandCollapseProvider(); bool ExpandCollapseProvider_GetIsExpanded(); bool ExpandCollapseProvider_GetShowsMenu(); From 1b2d3948d07da0c6569b90b7634d58fcba2d3bd2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Jul 2023 00:41:16 +0200 Subject: [PATCH 19/57] Fix accessibilityWindow. Seems it was broken before and always would have returned null. --- native/Avalonia.Native/src/OSX/automation.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 4b325a092d..9fe0ff3c60 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -291,8 +291,8 @@ private: - (id)accessibilityWindow { - id topLevel = [self accessibilityTopLevelUIElement]; - return [topLevel isKindOfClass:[NSWindow class]] ? topLevel : nil; + auto rootPeer = _peer->GetVisualRoot(); + return [AvnAccessibilityElement acquire:rootPeer]; } - (BOOL)isAccessibilityExpanded From 953ec9b3ef595336dfbd7ba397c8ef7305635218 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 28 Jul 2023 07:38:17 +0300 Subject: [PATCH 20/57] Remove BypassLogicalChildrenManagement and use OnPropertyChanged --- src/Avalonia.Controls/ContentControl.cs | 30 ++++++------------- .../TransitioningContentControl.cs | 8 ++--- .../TransitioningContentControlTests.cs | 9 ++++-- 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 47f7598900..b278942f84 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -40,11 +40,6 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = AvaloniaProperty.Register(nameof(VerticalContentAlignment)); - static ContentControl() - { - ContentProperty.Changed.AddClassHandler((x, e) => x.ContentChanged(e)); - } - /// /// Gets or sets the content to display. /// @@ -95,22 +90,20 @@ namespace Avalonia.Controls /// IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; - /// - /// Determine whether manage his LogicalChildren - ///(default behavior) himself, or leaves the management to the inherited control. - /// - /// - /// The default value is false, So the manages itself, - /// if you want to bypass this behavior and manage LogicalChildren yourself, set - /// the to true. - /// - protected virtual bool BypassLogicalChildrenManagement => false; - /// bool IContentPresenterHost.RegisterContentPresenter(ContentPresenter presenter) { return RegisterContentPresenter(presenter); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ContentProperty) + { + ContentChanged(change); + } + } /// /// Called when an is registered with the control. @@ -129,11 +122,6 @@ namespace Avalonia.Controls private void ContentChanged(AvaloniaPropertyChangedEventArgs e) { - if (BypassLogicalChildrenManagement) - { - return; - } - if (e.OldValue is ILogical oldChild) { LogicalChildren.Remove(oldChild); diff --git a/src/Avalonia.Controls/TransitioningContentControl.cs b/src/Avalonia.Controls/TransitioningContentControl.cs index c113afde17..0b04866d64 100644 --- a/src/Avalonia.Controls/TransitioningContentControl.cs +++ b/src/Avalonia.Controls/TransitioningContentControl.cs @@ -36,9 +36,6 @@ public class TransitioningContentControl : ContentControl set => SetValue(PageTransitionProperty, value); } - /// - protected override bool BypassLogicalChildrenManagement => true; - protected override Size ArrangeOverride(Size finalSize) { var result = base.ArrangeOverride(finalSize); @@ -101,12 +98,13 @@ public class TransitioningContentControl : ContentControl protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(change); - if (change.Property == ContentProperty) { UpdateContent(true); + return; } + + base.OnPropertyChanged(change); } private void UpdateContent(bool withTransition) diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs index 210b24b3ad..47e53676d8 100644 --- a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs @@ -184,7 +184,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Logical_Children_Dont_Duplicated() + public void Logical_Children_Should_Not_Be_Duplicated() { using var app = Start(); var (target, transition) = CreateTarget(""); @@ -194,18 +194,21 @@ namespace Avalonia.Controls.UnitTests target.Content = childControl; Assert.Equal(1, target.LogicalChildren.Count); + Assert.Equal(target.LogicalChildren[0], childControl); } [Fact] - public void First_Presenter_Register_TCC_As_Host() + public void First_Presenter_Should_Register_TCC_As_His_Host() { using var app = Start(); var (target, transition) = CreateTarget(""); + target.PageTransition = null; var childControl = new Control(); target.Presenter!.Content = childControl; - Assert.Contains(childControl, target.LogicalChildren); + Assert.Equal(1, target.LogicalChildren.Count); + Assert.Equal(target.LogicalChildren[0], childControl); } private static IDisposable Start() From b780c77260fa806bfc288e41ad46070785f758f9 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 28 Jul 2023 07:39:28 +0300 Subject: [PATCH 21/57] typo --- src/Avalonia.Controls/ContentControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index b278942f84..3564ce1faa 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -95,6 +95,7 @@ namespace Avalonia.Controls { return RegisterContentPresenter(presenter); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); From 90e3760c0f8e1c346a4b9d2d0f5a17f0fadbef9d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Jul 2023 09:19:42 +0200 Subject: [PATCH 22/57] Use interface instead of concrete class. --- src/Avalonia.Native/AvnAutomationPeer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index d2d93b69a9..af4958b02f 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -22,8 +22,8 @@ namespace Avalonia.Native { _inner = inner; _inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged(); - if (inner is WindowBaseAutomationPeer window) - window.FocusChanged += (_, _) => Node?.FocusChanged(); + if (inner is IRootProvider root) + root.FocusChanged += (_, _) => Node?.FocusChanged(); } ~AvnAutomationPeer() => Node?.Dispose(); From ae82bc1b8f39362dbfec7412c2a5381a92998359 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Jul 2023 11:19:57 +0200 Subject: [PATCH 23/57] Fix GetVisualRootCore. Needs to check for `IRootProvider`. Fixes integration tests on Windows. --- .../Automation/Peers/AutomationPeer.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index a264909ba6..fb7cdd87ed 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Automation.Provider; namespace Avalonia.Automation.Peers { @@ -254,17 +255,16 @@ namespace Avalonia.Automation.Peers protected virtual AutomationPeer? GetVisualRootCore() { - var parent = GetParent(); + var peer = this; + var parent = peer.GetParent(); - while (parent != null) + while (peer.GetProvider() is null && parent is not null) { - var nextParent = parent.GetParent(); - if (nextParent == null) - return parent; - parent = nextParent; + peer = parent; + parent = peer.GetParent(); } - return null; + return peer; } From 6b3db2a3f56231075042983d01272605974a6dee Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 28 Jul 2023 13:15:30 +0200 Subject: [PATCH 24/57] Shortcut finding the visual root for controls. --- .../Automation/Peers/ControlAutomationPeer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index c19d887230..69f267a605 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -120,6 +120,13 @@ namespace Avalonia.Automation.Peers return _parent; } + protected override AutomationPeer? GetVisualRootCore() + { + if (Owner.GetVisualRoot() is Control c) + return CreatePeerForElement(c); + return null; + } + /// /// Invalidates the peer's children and causes a re-read from . /// From adcaf6a317038033391f3d6a22e4b702118eea1b Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Thu, 3 Aug 2023 22:38:52 +0300 Subject: [PATCH 25/57] fix: try 1 --- src/Avalonia.Controls/TopLevel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 04a5a0e6aa..66e402d642 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -591,6 +591,7 @@ namespace Avalonia.Controls Renderer.SceneInvalidated -= SceneInvalidated; // We need to wait for the renderer to complete any in-flight operations Renderer.Dispose(); + StopRendering(); Debug.Assert(PlatformImpl != null); // The PlatformImpl is completely invalid at this point From b0f6d17c558fd7ece0c3d56d090433087de66871 Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 00:31:33 +0300 Subject: [PATCH 26/57] unit tests added --- .../WindowDataContextTests.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/Avalonia.LeakTests/WindowDataContextTests.cs diff --git a/tests/Avalonia.LeakTests/WindowDataContextTests.cs b/tests/Avalonia.LeakTests/WindowDataContextTests.cs new file mode 100644 index 0000000000..239b090515 --- /dev/null +++ b/tests/Avalonia.LeakTests/WindowDataContextTests.cs @@ -0,0 +1,70 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Reactive; +using Avalonia.Threading; +using Avalonia.UnitTests; +using JetBrains.dotMemoryUnit; +using Xunit; +using Xunit.Abstractions; + +namespace Avalonia.LeakTests; + +internal class ViewModelForDisposingTest +{ + ~ViewModelForDisposingTest() { ; } +} + +[DotMemoryUnit(FailIfRunWithoutSupport = false)] +public class WindowDataContextTests +{ + public WindowDataContextTests(ITestOutputHelper atr) + { + DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_With_Lifetime() + { + static IDisposable Run() + { + var unitTestApp = UnitTestApplication.Start(TestServices.StyledWindow); + var lifetime = new ClassicDesktopStyleApplicationLifetime(); + lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + + return Disposable.Create(lifetime, lt => lt.Shutdown()) + .DisposeWith(new CompositeDisposable(lifetime, unitTestApp)); + } + + using var _ = Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_Without_Lifetime() + { + static void Run() + { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + } + + Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } +} From c418442d1345ee8ea5ed113ae9453bd740088c08 Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 17:17:57 +0300 Subject: [PATCH 27/57] Revert "unit tests added" This reverts commit b0f6d17c558fd7ece0c3d56d090433087de66871. --- .../WindowDataContextTests.cs | 70 ------------------- 1 file changed, 70 deletions(-) delete mode 100644 tests/Avalonia.LeakTests/WindowDataContextTests.cs diff --git a/tests/Avalonia.LeakTests/WindowDataContextTests.cs b/tests/Avalonia.LeakTests/WindowDataContextTests.cs deleted file mode 100644 index 239b090515..0000000000 --- a/tests/Avalonia.LeakTests/WindowDataContextTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Reactive; -using Avalonia.Threading; -using Avalonia.UnitTests; -using JetBrains.dotMemoryUnit; -using Xunit; -using Xunit.Abstractions; - -namespace Avalonia.LeakTests; - -internal class ViewModelForDisposingTest -{ - ~ViewModelForDisposingTest() { ; } -} - -[DotMemoryUnit(FailIfRunWithoutSupport = false)] -public class WindowDataContextTests -{ - public WindowDataContextTests(ITestOutputHelper atr) - { - DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); - } - - [Fact] - public void Window_DataContext_Disposed_After_Window_Close_With_Lifetime() - { - static IDisposable Run() - { - var unitTestApp = UnitTestApplication.Start(TestServices.StyledWindow); - var lifetime = new ClassicDesktopStyleApplicationLifetime(); - lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; - var window = new Window { DataContext = new ViewModelForDisposingTest() }; - window.Show(); - window.Close(); - - return Disposable.Create(lifetime, lt => lt.Shutdown()) - .DisposeWith(new CompositeDisposable(lifetime, unitTestApp)); - } - - using var _ = Run(); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); - GC.Collect(); - - dotMemory.Check(m => Assert.Equal(0, - m.GetObjects(o => o.Type.Is()).ObjectsCount)); - } - - [Fact] - public void Window_DataContext_Disposed_After_Window_Close_Without_Lifetime() - { - static void Run() - { - using var _ = UnitTestApplication.Start(TestServices.StyledWindow); - var window = new Window { DataContext = new ViewModelForDisposingTest() }; - window.Show(); - window.Close(); - } - - Run(); - // Process all Loaded events to free control reference(s) - Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); - GC.Collect(); - - dotMemory.Check(m => Assert.Equal(0, - m.GetObjects(o => o.Type.Is()).ObjectsCount)); - } -} From ca2ca4ee9fae2e20155da6bd3148dc3e999fe57c Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 17:20:34 +0300 Subject: [PATCH 28/57] Revert "fix: try 1" This reverts commit adcaf6a317038033391f3d6a22e4b702118eea1b. --- src/Avalonia.Controls/TopLevel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 66e402d642..04a5a0e6aa 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -591,7 +591,6 @@ namespace Avalonia.Controls Renderer.SceneInvalidated -= SceneInvalidated; // We need to wait for the renderer to complete any in-flight operations Renderer.Dispose(); - StopRendering(); Debug.Assert(PlatformImpl != null); // The PlatformImpl is completely invalid at this point From 5b182890f3c3dece03e81c79ae1bdf889e85961c Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 17:27:31 +0300 Subject: [PATCH 29/57] Added unit tests for non-disposable DataContext issue (#12123) --- tests/Avalonia.LeakTests/DataContextTests.cs | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/Avalonia.LeakTests/DataContextTests.cs diff --git a/tests/Avalonia.LeakTests/DataContextTests.cs b/tests/Avalonia.LeakTests/DataContextTests.cs new file mode 100644 index 0000000000..d16f8c1f57 --- /dev/null +++ b/tests/Avalonia.LeakTests/DataContextTests.cs @@ -0,0 +1,70 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Reactive; +using Avalonia.Threading; +using Avalonia.UnitTests; +using JetBrains.dotMemoryUnit; +using Xunit; +using Xunit.Abstractions; + +namespace Avalonia.LeakTests; + +internal class ViewModelForDisposingTest +{ + ~ViewModelForDisposingTest() { ; } +} + +[DotMemoryUnit(FailIfRunWithoutSupport = false)] +public class DataContextTests +{ + public DataContextTests(ITestOutputHelper atr) + { + DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_With_Lifetime() + { + static IDisposable Run() + { + var unitTestApp = UnitTestApplication.Start(TestServices.StyledWindow); + var lifetime = new ClassicDesktopStyleApplicationLifetime(); + lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + + return Disposable.Create(lifetime, lt => lt.Shutdown()) + .DisposeWith(new CompositeDisposable(lifetime, unitTestApp)); + } + + using var _ = Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } + + [Fact] + public void Window_DataContext_Disposed_After_Window_Close_Without_Lifetime() + { + static void Run() + { + using var _ = UnitTestApplication.Start(TestServices.StyledWindow); + var window = new Window { DataContext = new ViewModelForDisposingTest() }; + window.Show(); + window.Close(); + } + + Run(); + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + GC.Collect(); + + dotMemory.Check(m => Assert.Equal(0, + m.GetObjects(o => o.Type.Is()).ObjectsCount)); + } +} From 628455ec3d3291083d76216de9d9b7934d1fcfcd Mon Sep 17 00:00:00 2001 From: flexxxxer Date: Sat, 5 Aug 2023 17:29:17 +0300 Subject: [PATCH 30/57] To Avalonia.Controls.TopLevel.HandleClosed method body was added Avalonia.Controls.TopLevel.StopRendering method call --- src/Avalonia.Controls/TopLevel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 04a5a0e6aa..66e402d642 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -591,6 +591,7 @@ namespace Avalonia.Controls Renderer.SceneInvalidated -= SceneInvalidated; // We need to wait for the renderer to complete any in-flight operations Renderer.Dispose(); + StopRendering(); Debug.Assert(PlatformImpl != null); // The PlatformImpl is completely invalid at this point From 8328cc79e2bbe0d1b038d689a47df1ddea907665 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 8 Aug 2023 15:25:00 +0600 Subject: [PATCH 31/57] Unwrap win32 data object --- src/Windows/Avalonia.Win32/DataObject.cs | 1 + src/Windows/Avalonia.Win32/OleDropTarget.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs index a215a0a322..df19cdc329 100644 --- a/src/Windows/Avalonia.Win32/DataObject.cs +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -103,6 +103,7 @@ namespace Avalonia.Win32 private IDataObject _wrapped; + public IDataObject Wrapped => _wrapped; public DataObject(IDataObject wrapped) { diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index a81652ffc2..94d744301a 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -217,7 +217,7 @@ namespace Avalonia.Win32 if (MicroComRuntime.TryUnwrapManagedObject(pDataObj) is DataObject dataObject) { - return dataObject; + return dataObject.Wrapped; } return new OleDataObject(pDataObj); } From 2c889d558acf18b6e30714a56dc4158d4d9fe647 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 10 Aug 2023 11:28:10 +0100 Subject: [PATCH 32/57] Fixes #YOUTRACK-HDSW-24 --- packages/Avalonia/Avalonia.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 4258572df7..ebfa325067 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -5,7 +5,7 @@ - + all From 11971a0431bdda95581c8f4914c164f87e8acab1 Mon Sep 17 00:00:00 2001 From: adirh Date: Sat, 12 Aug 2023 16:42:50 +0300 Subject: [PATCH 33/57] Fixed memory leaks in ContextMenu.cs --- src/Avalonia.Controls/ContextMenu.cs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index e3a419d7b3..7fab36d931 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -114,7 +114,6 @@ namespace Avalonia.Controls /// static ContextMenu() { - ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); PlacementProperty.OverrideDefaultValue(PlacementMode.Pointer); ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue(AccessibilityView.Control); @@ -216,16 +215,16 @@ namespace Avalonia.Controls if (e.OldValue is ContextMenu oldMenu) { control.ContextRequested -= ControlContextRequested; + control.AttachedToVisualTree -= ControlOnAttachedToVisualTree; control.DetachedFromVisualTree -= ControlDetachedFromVisualTree; oldMenu._attachedControls?.Remove(control); ((ISetLogicalParent?)oldMenu._popup)?.SetParent(null); } - if (e.NewValue is ContextMenu newMenu) + if (e.NewValue is ContextMenu) { - newMenu._attachedControls ??= new List(); - newMenu._attachedControls.Add(control); control.ContextRequested += ControlContextRequested; + control.AttachedToVisualTree += ControlOnAttachedToVisualTree; control.DetachedFromVisualTree += ControlDetachedFromVisualTree; } } @@ -428,11 +427,20 @@ namespace Avalonia.Controls e.Handled = true; } } + + + private static void ControlOnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + if (sender is Control { ContextMenu: {} contextMenu } control) + { + contextMenu._attachedControls ??= new List(); + contextMenu._attachedControls.Add(control); + } + } private static void ControlDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e) { - if (sender is Control control - && control.ContextMenu is ContextMenu contextMenu) + if (sender is Control { ContextMenu: { } contextMenu } control) { if (contextMenu._popup?.Parent == control) { @@ -440,6 +448,7 @@ namespace Avalonia.Controls } contextMenu.Close(); + contextMenu._attachedControls?.Remove(control); } } From 5f7d823b59c96473d048f5c66ab098c36bd055bb Mon Sep 17 00:00:00 2001 From: adirh Date: Sat, 12 Aug 2023 17:26:02 +0300 Subject: [PATCH 34/57] Added unit test --- tests/Avalonia.LeakTests/ControlTests.cs | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index ab8e4377b6..9a4bcb1808 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -583,6 +583,48 @@ namespace Avalonia.LeakTests Assert.Equal(initialMenuItemCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } } + + [Fact] + public void Attached_Control_From_ContextMenu_Is_Freed() + { + using (Start()) + { + var contextMenu = new ContextMenu(); + Func run = () => + { + var window = new Window + { + Content = new TextBlock + { + ContextMenu = contextMenu + } + }; + + window.Show(); + + // Do a layout and make sure that TextBlock gets added to visual tree with + // its render transform. + window.LayoutManager.ExecuteInitialLayoutPass(); + var textBlock = Assert.IsType(window.Presenter.Child); + Assert.IsType(textBlock.RenderTransform); + + // Clear the content and ensure the TextBlock is removed. + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + // Process all Loaded events to free control reference(s) + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + } [Fact] public void Standalone_ContextMenu_Is_Freed() From 1d484685370d7ddb60259b74f1f24630a975f17b Mon Sep 17 00:00:00 2001 From: adirh Date: Sat, 12 Aug 2023 17:30:51 +0300 Subject: [PATCH 35/57] Added check for when control already attached to visual tree --- src/Avalonia.Controls/ContextMenu.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 7fab36d931..6300239e12 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -227,6 +227,11 @@ namespace Avalonia.Controls control.AttachedToVisualTree += ControlOnAttachedToVisualTree; control.DetachedFromVisualTree += ControlDetachedFromVisualTree; } + + if (control.IsAttachedToVisualTree) + { + AttachControlToContextMenu(control); + } } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -431,7 +436,12 @@ namespace Avalonia.Controls private static void ControlOnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) { - if (sender is Control { ContextMenu: {} contextMenu } control) + AttachControlToContextMenu(sender); + } + + private static void AttachControlToContextMenu(object? sender) + { + if (sender is Control { ContextMenu: { } contextMenu } control) { contextMenu._attachedControls ??= new List(); contextMenu._attachedControls.Add(control); From 038c96375037bc9fe5e72382c5697bac73aa18f2 Mon Sep 17 00:00:00 2001 From: adirh Date: Sat, 12 Aug 2023 21:39:45 +0300 Subject: [PATCH 36/57] Fixed unit test --- tests/Avalonia.LeakTests/ControlTests.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 9a4bcb1808..c6f9c5eae0 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -602,11 +602,9 @@ namespace Avalonia.LeakTests window.Show(); - // Do a layout and make sure that TextBlock gets added to visual tree with - // its render transform. + // Do a layout and make sure that TextBlock gets added to visual tree. window.LayoutManager.ExecuteInitialLayoutPass(); - var textBlock = Assert.IsType(window.Presenter.Child); - Assert.IsType(textBlock.RenderTransform); + Assert.IsType(window.Presenter.Child); // Clear the content and ensure the TextBlock is removed. window.Content = null; From ea946b2b7ceb67737fa432d39b5e9af33329a01b Mon Sep 17 00:00:00 2001 From: Flithor <23412916+Flithor@users.noreply.github.com> Date: Tue, 15 Aug 2023 09:22:48 +0800 Subject: [PATCH 37/57] Update Fluent.xaml --- .../Themes/Fluent.xaml | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index 0cc620dae9..e6831faf87 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -170,26 +170,28 @@ CornerRadius="{TemplateBinding CornerRadius}"> - - - - - - - - - - + + + + + + + + + + + Date: Tue, 15 Aug 2023 09:27:28 +0800 Subject: [PATCH 38/57] Update Fluent.xaml --- src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index e6831faf87..2d40721bbf 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -170,7 +170,7 @@ CornerRadius="{TemplateBinding CornerRadius}"> - @@ -191,7 +191,7 @@ Fill="{TemplateBinding Foreground}" Stretch="Uniform" /> - + Date: Tue, 15 Aug 2023 18:37:33 +0300 Subject: [PATCH 39/57] Set PreserveSig to true for OleGetClipboard --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 68f3e3c670..226ed2a406 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1509,7 +1509,7 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern IntPtr SetClipboardData(ClipboardFormat uFormat, IntPtr hMem); - [DllImport("ole32.dll", PreserveSig = false)] + [DllImport("ole32.dll", PreserveSig = true)] public static extern int OleGetClipboard(out IntPtr dataObject); [DllImport("ole32.dll", PreserveSig = true)] From 74a5e13d2e20bf3d31238e050c4d5775c7c5b137 Mon Sep 17 00:00:00 2001 From: adirh Date: Tue, 15 Aug 2023 21:23:42 +0300 Subject: [PATCH 40/57] Trigger devops build again --- src/Avalonia.Controls/ContextMenu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 6300239e12..78f6c5f77d 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -230,7 +230,7 @@ namespace Avalonia.Controls if (control.IsAttachedToVisualTree) { - AttachControlToContextMenu(control); + AttachControlToContextMenu(control); } } From e0ffb920a9926618d49ee8c647cd50367da4653b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 16 Aug 2023 13:26:40 +0100 Subject: [PATCH 41/57] fix macos keydown and textinput events being raised in the wrong order. --- native/Avalonia.Native/src/OSX/AvnView.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index a4999b8df3..ea6ba93fdb 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -538,10 +538,10 @@ { _lastKeyHandled = false; - [[self inputContext] handleEvent:event]; + [self keyboardEvent:event withType:KeyDown]; if(!_lastKeyHandled){ - [self keyboardEvent:event withType:KeyDown]; + [[self inputContext] handleEvent:event]; } } From eafaa6e584a5fcffda9ab2546888822c524b8b0d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 16 Aug 2023 16:38:35 +0100 Subject: [PATCH 42/57] remove legacy nuget feed no longer reachable. --- NuGet.Config | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/NuGet.Config b/NuGet.Config index 7d2bd8abd2..2042fea360 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -3,8 +3,7 @@ - - + From 839de5966b3adc67ee549ee2d4422126194f3e05 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 16 Aug 2023 16:48:30 +0100 Subject: [PATCH 43/57] remove all traces of old api diff. --- Avalonia.sln | 1 - build/ApiDiff.props | 13 ------------- src/Avalonia.Base/Avalonia.Base.csproj | 1 - .../Avalonia.Controls.ColorPicker.csproj | 1 - .../Avalonia.Controls.DataGrid.csproj | 1 - .../Avalonia.Controls.ItemsRepeater.csproj | 1 - src/Avalonia.Controls/Avalonia.Controls.csproj | 1 - .../Avalonia.DesignerSupport.csproj | 1 - src/Avalonia.Desktop/Avalonia.Desktop.csproj | 1 - .../Avalonia.Diagnostics.csproj | 1 - src/Avalonia.Dialogs/Avalonia.Dialogs.csproj | 1 - src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj | 1 - .../Avalonia.Remote.Protocol.csproj | 3 +-- .../Avalonia.Themes.Fluent.csproj | 1 - .../Avalonia.Themes.Simple.csproj | 1 - .../Avalonia.Headless.NUnit.csproj | 3 +-- .../Avalonia.Headless.Vnc.csproj | 1 - .../Avalonia.Headless.XUnit.csproj | 1 - .../Avalonia.Headless/Avalonia.Headless.csproj | 1 - .../Avalonia.Markup.Xaml.csproj | 1 - src/Markup/Avalonia.Markup/Avalonia.Markup.csproj | 1 - 21 files changed, 2 insertions(+), 35 deletions(-) delete mode 100644 build/ApiDiff.props diff --git a/Avalonia.sln b/Avalonia.sln index d5419365ac..98ad0cadae 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -91,7 +91,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "s EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}" ProjectSection(SolutionItems) = preProject - build\ApiDiff.props = build\ApiDiff.props build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props build\Base.props = build\Base.props build\Binding.props = build\Binding.props diff --git a/build/ApiDiff.props b/build/ApiDiff.props deleted file mode 100644 index b0b1942f60..0000000000 --- a/build/ApiDiff.props +++ /dev/null @@ -1,13 +0,0 @@ - - - 0.10.0 - $(PackageId) - Avalonia - false - - - - - - - diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 16eb09de65..9aeb71f649 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -15,7 +15,6 @@ - diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj index 5a31053bdc..8a36e6900a 100644 --- a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj +++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj @@ -17,7 +17,6 @@ - diff --git a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj index 6556ce721e..6c1e019603 100644 --- a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj +++ b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj @@ -14,7 +14,6 @@ - diff --git a/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj b/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj index 1ec0ee33a7..b7739bda8d 100644 --- a/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj +++ b/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 304454777f..0dda861448 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -6,7 +6,6 @@ - diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index c5255b22cd..0c1b3e8a45 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -19,5 +19,4 @@ - diff --git a/src/Avalonia.Desktop/Avalonia.Desktop.csproj b/src/Avalonia.Desktop/Avalonia.Desktop.csproj index d180e6e34d..12cc977c84 100644 --- a/src/Avalonia.Desktop/Avalonia.Desktop.csproj +++ b/src/Avalonia.Desktop/Avalonia.Desktop.csproj @@ -12,7 +12,6 @@ - diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index 65d1bea298..135bc1b685 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -24,7 +24,6 @@ - diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index be4bae26f1..3a2c18ac24 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -19,7 +19,6 @@ - diff --git a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj index 4cae8e82df..30b5d80fbc 100644 --- a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj +++ b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj @@ -9,7 +9,6 @@ - diff --git a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj index baeb8820f8..13d1ff9651 100644 --- a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj +++ b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj @@ -10,6 +10,5 @@ - - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 660661fc94..e62abff18b 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -11,7 +11,6 @@ - diff --git a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj index 39da9a747f..9864cf24e0 100644 --- a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj +++ b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj @@ -11,7 +11,6 @@ - diff --git a/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj b/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj index 49f1de31f2..d176d74b5f 100644 --- a/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj +++ b/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj @@ -12,8 +12,7 @@ - - + diff --git a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj index 1f06f28687..2020dca482 100644 --- a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj +++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -10,7 +10,6 @@ - diff --git a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj index 4ab70eb07d..85879faa4c 100644 --- a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj +++ b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index 893cb0074c..fe071b594f 100644 --- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -7,7 +7,6 @@ - diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 618379757b..d827530207 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -65,7 +65,6 @@ - diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index d041e7d2e6..29583b6cb8 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -13,7 +13,6 @@ - From 815545dfcd758ece930125d5072e26c8f421c6dc Mon Sep 17 00:00:00 2001 From: stepan_govorko Date: Wed, 16 Aug 2023 13:13:15 +0200 Subject: [PATCH 44/57] Consider Clip.Bounds in clipping calculation in ServerCompositionVisual; In addition, the ServerCompositionVisual now tracks changes to the clip property, allowing for re-calculation when the clip is altered. --- ...ServerCompositionVisual.DirtyProperties.cs | 1 + .../Server/ServerCompositionVisual.cs | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs index c1037d5c67..51414c2250 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs @@ -38,6 +38,7 @@ partial class ServerCompositionVisual CompositionVisualChangedFields.Size | CompositionVisualChangedFields.SizeAnimated | CompositionVisualChangedFields.ClipToBounds + | CompositionVisualChangedFields.Clip | CompositionVisualChangedFields.ClipToBoundsAnimated; partial void OnFieldsDeserialized(CompositionVisualChangedFields changed) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index 45515a37e2..fd1e2165b7 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -182,11 +182,24 @@ namespace Avalonia.Rendering.Composition.Server if (_clipSizeDirty || positionChanged) { - _transformedClipBounds = ClipToBounds - ? new Rect(new Size(Size.X, Size.Y)) - .TransformToAABB(GlobalTransformMatrix) - : null; + Rect? transformedVisualBounds = null; + Rect? transformedClipBounds = null; + if (ClipToBounds) + transformedVisualBounds = new Rect(new Size(Size.X, Size.Y)).TransformToAABB(GlobalTransformMatrix); + + if (Clip != null) + transformedClipBounds = Clip.Bounds.TransformToAABB(GlobalTransformMatrix); + + if (transformedVisualBounds != null && transformedClipBounds != null) + _transformedClipBounds = transformedVisualBounds.Value.Intersect(transformedClipBounds.Value); + else if (transformedVisualBounds != null) + _transformedClipBounds = transformedVisualBounds; + else if (transformedClipBounds != null) + _transformedClipBounds = transformedClipBounds; + else + _transformedClipBounds = null; + _clipSizeDirty = false; } From 77dc35bf469fbc583d9c09add0fa255e9badd1bc Mon Sep 17 00:00:00 2001 From: stepan_govorko Date: Thu, 17 Aug 2023 09:26:25 +0200 Subject: [PATCH 45/57] Introduced counting of rendered visuals in ServerCompositionVisual and added relevant unit tests in CompositorInvalidationClippingTests. The new tests ensure that visuals that are not in dirty rect are rendered correctly with different ClipToBounds and Clip geometry parameters. --- .../ICompositionTargetDebugEvents.cs | 2 + .../Server/ServerCompositionVisual.cs | 1 + .../CompositorInvalidationClippingTests.cs | 60 +++++++++++++++++++ .../CompositorTestServices.cs | 17 +++++- 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs index c830ca2c49..cfbce221d6 100644 --- a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs +++ b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs @@ -2,5 +2,7 @@ namespace Avalonia.Rendering.Composition; internal interface ICompositionTargetDebugEvents { + public int RenderedVisuals { get; } + void IncrementRenderedVisuals(); void RectInvalidated(Rect rc); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index fd1e2165b7..aeb228282e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -38,6 +38,7 @@ namespace Avalonia.Rendering.Composition.Server return; Root!.RenderedVisuals++; + Root!.DebugEvents?.IncrementRenderedVisuals(); var boundsRect = new Rect(new Size(Size.X, Size.Y)); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs new file mode 100644 index 0000000000..1de2cfa717 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs @@ -0,0 +1,60 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Base.UnitTests.Rendering; + +public class CompositorInvalidationClippingTests : CompositorTestsBase +{ + [Fact] + public void Siblings_Should_Be_Rendered_On_Invalidate_Without_ClipToBounds() + { + AssertRenderedVisuals(clipToBounds: false, clipGeometry: false, expectedRenderedVisualsCount: 4); + } + + [Fact] + public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_ClipToBounds() + { + AssertRenderedVisuals(clipToBounds: true, clipGeometry: false, expectedRenderedVisualsCount: 3); + } + + [Fact] + public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_Clip() + { + AssertRenderedVisuals(clipToBounds: false, clipGeometry: true, expectedRenderedVisualsCount: 3); + } + + private void AssertRenderedVisuals(bool clipToBounds, bool clipGeometry, int expectedRenderedVisualsCount) + { + using (var s = new CompositorCanvas()) + { + //#1 visual to render is root + //#2 visual to render is s.Canvas + + //#3 visual to render + s.Canvas.Children.Add(new Border() + { + [Canvas.LeftProperty] = 0, [Canvas.TopProperty] = 0, + Width = 20, Height = 10, + Background = Brushes.Red, + ClipToBounds = clipToBounds, + Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null + }); + + //#4 visual to render + s.Canvas.Children.Add(new Border() + { + [Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50, + Width = 20, Height = 10, + Background = Brushes.Red, + ClipToBounds = clipToBounds, + Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null + }); + s.RunJobs(); + s.Events.Reset(); + s.Canvas.Children[0].IsVisible = false; + s.RunJobs(); + s.AssertRenderedVisuals(expectedRenderedVisualsCount); + } + } +} diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs index 53fd610a17..00645259a5 100644 --- a/tests/Avalonia.UnitTests/CompositorTestServices.cs +++ b/tests/Avalonia.UnitTests/CompositorTestServices.cs @@ -89,6 +89,13 @@ public class CompositorTestServices : IDisposable Events.Rects.Clear(); } + public void AssertRenderedVisuals(int renderVisuals) + { + RunJobs(); + Assert.Equal(Events.RenderedVisuals, renderVisuals); + Events.Rects.Clear(); + } + public void AssertHitTest(double x, double y, Func filter, params object[] expected) => AssertHitTest(new Point(x, y), filter, expected); @@ -110,6 +117,13 @@ public class CompositorTestServices : IDisposable { public List Rects = new(); + public int RenderedVisuals { get; private set; } + + public void IncrementRenderedVisuals() + { + RenderedVisuals++; + } + public void RectInvalidated(Rect rc) { Rects.Add(rc); @@ -118,6 +132,7 @@ public class CompositorTestServices : IDisposable public void Reset() { Rects.Clear(); + RenderedVisuals = 0; } } @@ -218,4 +233,4 @@ public class DispatcherCompositorScheduler : ICompositorScheduler { Dispatcher.UIThread.Post(() => compositor.Commit(), DispatcherPriority.UiThreadRender); } -} \ No newline at end of file +} From f5c93a50abb12ce6c1e4bf40200a1eaf916c9f12 Mon Sep 17 00:00:00 2001 From: stepan_govorko Date: Thu, 17 Aug 2023 09:51:04 +0200 Subject: [PATCH 46/57] added more comments --- .../CompositorInvalidationClippingTests.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs index 1de2cfa717..c158ff4e75 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs @@ -3,22 +3,27 @@ using Avalonia.Media; using Xunit; namespace Avalonia.Base.UnitTests.Rendering; - +/// +/// Test class that verifies how clipping influences rendering in the compositor +/// public class CompositorInvalidationClippingTests : CompositorTestsBase { [Fact] + // Test case: When the ClipToBounds is false, all visuals should be rendered public void Siblings_Should_Be_Rendered_On_Invalidate_Without_ClipToBounds() { AssertRenderedVisuals(clipToBounds: false, clipGeometry: false, expectedRenderedVisualsCount: 4); } [Fact] + // Test case: When the ClipToBounds is true, only visuals within the clipped boundary should be rendered public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_ClipToBounds() { AssertRenderedVisuals(clipToBounds: true, clipGeometry: false, expectedRenderedVisualsCount: 3); } [Fact] + // Test case: When the Clip is used, only visuals within the clip geometry should be rendered public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_Clip() { AssertRenderedVisuals(clipToBounds: false, clipGeometry: true, expectedRenderedVisualsCount: 3); @@ -28,10 +33,10 @@ public class CompositorInvalidationClippingTests : CompositorTestsBase { using (var s = new CompositorCanvas()) { - //#1 visual to render is root - //#2 visual to render is s.Canvas + //#1 visual is top level + //#2 visual is s.Canvas - //#3 visual to render + //#3 visual is border1 s.Canvas.Children.Add(new Border() { [Canvas.LeftProperty] = 0, [Canvas.TopProperty] = 0, @@ -41,7 +46,7 @@ public class CompositorInvalidationClippingTests : CompositorTestsBase Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null }); - //#4 visual to render + //#4 visual is border2 s.Canvas.Children.Add(new Border() { [Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50, @@ -52,8 +57,11 @@ public class CompositorInvalidationClippingTests : CompositorTestsBase }); s.RunJobs(); s.Events.Reset(); + + //invalidate border1 s.Canvas.Children[0].IsVisible = false; s.RunJobs(); + s.AssertRenderedVisuals(expectedRenderedVisualsCount); } } From d64502e2e7c968f1d1140d22f742b8ea948af075 Mon Sep 17 00:00:00 2001 From: Bill Henning Date: Thu, 17 Aug 2023 16:21:33 -0400 Subject: [PATCH 47/57] Fix for Track not arranging after IsDirectionReversed property changed. --- src/Avalonia.Controls/Primitives/Track.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index f9aa5fc7a0..d823455e13 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -52,7 +52,7 @@ namespace Avalonia.Controls.Primitives ThumbProperty.Changed.AddClassHandler((x, e) => x.ThumbChanged(e)); IncreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); DecreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); - AffectsArrange(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); + AffectsArrange(IsDirectionReversedProperty, MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); } public Track() From fa6939aa39496bd3e468b2cd6eec090fffc7dda8 Mon Sep 17 00:00:00 2001 From: stepan_govorko Date: Fri, 18 Aug 2023 06:22:27 +0200 Subject: [PATCH 48/57] The unneeded public access modifier was removed from the RenderedVisuals property in the ICompositionTargetDebugEvents interface. --- .../Rendering/Composition/ICompositionTargetDebugEvents.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs index cfbce221d6..27aca436b8 100644 --- a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs +++ b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs @@ -2,7 +2,7 @@ namespace Avalonia.Rendering.Composition; internal interface ICompositionTargetDebugEvents { - public int RenderedVisuals { get; } + int RenderedVisuals { get; } void IncrementRenderedVisuals(); void RectInvalidated(Rect rc); } From f8956618790b80d178b04d36bcf9211273ca6c2e Mon Sep 17 00:00:00 2001 From: Hanjiang Yu Date: Fri, 18 Aug 2023 16:08:00 +0800 Subject: [PATCH 50/57] Fix main loop cancellation from another thread --- .../src/OSX/platformthreading.mm | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index d80df68fea..56b7ce97e0 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -17,7 +17,6 @@ public: Cancelled = true; if(Running) { - Running = false; if(![NSThread isMainThread]) { AddRef(); @@ -28,22 +27,22 @@ public: }); return; }; + + Running = false; if(IsApp) [NSApp stop:nil]; - else - { - // Wakeup the event loop - NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined - location:NSMakePoint(0, 0) - modifierFlags:0 - timestamp:0 - windowNumber:0 - context:nil - subtype:0 - data1:0 - data2:0]; - [NSApp postEvent:event atStart:YES]; - } + + // Wakeup the event loop + NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:NSMakePoint(0, 0) + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:0 + data1:0 + data2:0]; + [NSApp postEvent:event atStart:YES]; } }; }; From 09250ec9c4ed78a50fae2143e943bcba85edf0d6 Mon Sep 17 00:00:00 2001 From: Bill Henning Date: Fri, 18 Aug 2023 08:47:04 -0400 Subject: [PATCH 51/57] Updated ToggleSplitButton.IsChecked to bind TwoWay by default, same as ToggleButton. --- src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs index ff8bd5dc0b..e6f0abcad0 100644 --- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs @@ -2,6 +2,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Data; using Avalonia.Interactivity; using Avalonia.Styling; @@ -36,8 +37,8 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IsCheckedProperty = - AvaloniaProperty.Register( - nameof(IsChecked)); + AvaloniaProperty.Register(nameof(IsChecked), false, + defaultBindingMode: BindingMode.TwoWay); /// /// Initializes a new instance of the class. From 4b5c680a2644cc70ced48953cda0b94ed8bd29f6 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 18 Aug 2023 16:22:27 +0200 Subject: [PATCH 52/57] Introduce RenderOptions.RequiresFullOpacityHandling --- src/Avalonia.Base/Media/RenderOptions.cs | 45 ++++++++++++++++---- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 28 +++++++----- 2 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Base/Media/RenderOptions.cs b/src/Avalonia.Base/Media/RenderOptions.cs index 639498543b..1ac2520919 100644 --- a/src/Avalonia.Base/Media/RenderOptions.cs +++ b/src/Avalonia.Base/Media/RenderOptions.cs @@ -8,12 +8,13 @@ namespace Avalonia.Media public EdgeMode EdgeMode { get; init; } public TextRenderingMode TextRenderingMode { get; init; } public BitmapBlendingMode BitmapBlendingMode { get; init; } + public bool? RequiresFullOpacityHandling { get; init; } /// /// Gets the value of the BitmapInterpolationMode attached property for a visual. /// /// The control. - /// The control's left coordinate. + /// The value. public static BitmapInterpolationMode GetBitmapInterpolationMode(Visual visual) { return visual.RenderOptions.BitmapInterpolationMode; @@ -23,7 +24,7 @@ namespace Avalonia.Media /// Sets the value of the BitmapInterpolationMode attached property for a visual. /// /// The control. - /// The left value. + /// The value. public static void SetBitmapInterpolationMode(Visual visual, BitmapInterpolationMode value) { visual.RenderOptions = visual.RenderOptions with { BitmapInterpolationMode = value }; @@ -33,7 +34,7 @@ namespace Avalonia.Media /// Gets the value of the BitmapBlendingMode attached property for a visual. /// /// The control. - /// The control's left coordinate. + /// The value. public static BitmapBlendingMode GetBitmapBlendingMode(Visual visual) { return visual.RenderOptions.BitmapBlendingMode; @@ -53,7 +54,7 @@ namespace Avalonia.Media /// Gets the value of the EdgeMode attached property for a visual. /// /// The control. - /// The control's left coordinate. + /// The value. public static EdgeMode GetEdgeMode(Visual visual) { return visual.RenderOptions.EdgeMode; @@ -63,7 +64,7 @@ namespace Avalonia.Media /// Sets the value of the EdgeMode attached property for a visual. /// /// The control. - /// The left value. + /// The value. public static void SetEdgeMode(Visual visual, EdgeMode value) { visual.RenderOptions = visual.RenderOptions with { EdgeMode = value }; @@ -73,7 +74,7 @@ namespace Avalonia.Media /// Gets the value of the TextRenderingMode attached property for a visual. /// /// The control. - /// The control's left coordinate. + /// The value. public static TextRenderingMode GetTextRenderingMode(Visual visual) { return visual.RenderOptions.TextRenderingMode; @@ -83,12 +84,32 @@ namespace Avalonia.Media /// Sets the value of the TextRenderingMode attached property for a visual. /// /// The control. - /// The left value. + /// The value. public static void SetTextRenderingMode(Visual visual, TextRenderingMode value) { visual.RenderOptions = visual.RenderOptions with { TextRenderingMode = value }; } + /// + /// Gets the value of the RequiresFullOpacityHandling attached property for a visual. + /// + /// The control. + /// The value. + public static bool? GetRequiresFullOpacityHandling(Visual visual) + { + return visual.RenderOptions.RequiresFullOpacityHandling; + } + + /// + /// Sets the value of the RequiresFullOpacityHandling attached property for a visual. + /// + /// The control. + /// The value. + public static void SetRequiresFullOpacityHandling(Visual visual, bool? value) + { + visual.RenderOptions = visual.RenderOptions with { RequiresFullOpacityHandling = value }; + } + public RenderOptions MergeWith(RenderOptions other) { var bitmapInterpolationMode = BitmapInterpolationMode; @@ -119,12 +140,20 @@ namespace Avalonia.Media bitmapBlendingMode = other.BitmapBlendingMode; } + var requiresFullOpacityHandling = RequiresFullOpacityHandling; + + if (requiresFullOpacityHandling == null) + { + requiresFullOpacityHandling = other.RequiresFullOpacityHandling; + } + return new RenderOptions { BitmapInterpolationMode = bitmapInterpolationMode, EdgeMode = edgeMode, TextRenderingMode = textRenderingMode, - BitmapBlendingMode = bitmapBlendingMode + BitmapBlendingMode = bitmapBlendingMode, + RequiresFullOpacityHandling = requiresFullOpacityHandling }; } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index fbff4ab4e7..7b5cca0fde 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -189,7 +189,8 @@ namespace Avalonia.Skia var d = destRect.ToSKRect(); var paint = SKPaintCache.Shared.Get(); - paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * (_useOpacitySaveLayer ? 1 : _currentOpacity))); + + paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * currentOpacity)); paint.FilterQuality = RenderOptions.BitmapInterpolationMode.ToSKFilterQuality(); paint.BlendMode = RenderOptions.BitmapBlendingMode.ToSKBlendMode(); @@ -375,7 +376,7 @@ namespace Avalonia.Skia { if (boxShadow != default && !boxShadow.IsInset) { - using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) { var spread = (float)boxShadow.Spread; if (boxShadow.IsInset) @@ -432,7 +433,7 @@ namespace Avalonia.Skia { if (boxShadow != default && boxShadow.IsInset) { - using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, currentOpacity)) { var spread = (float)boxShadow.Spread; var offsetX = (float)boxShadow.OffsetX; @@ -592,8 +593,16 @@ namespace Avalonia.Skia { CheckLease(); - if(_useOpacitySaveLayer) + _opacityStack.Push(_currentOpacity); + + var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true; + + if (useOpacitySaveLayer) { + opacity = _currentOpacity * opacity; //Take current multiplied opacity + + _currentOpacity = 1; //Opacity is applied via layering + if (bounds.HasValue) { var rect = bounds.Value.ToSKRect(); @@ -606,7 +615,6 @@ namespace Avalonia.Skia } else { - _opacityStack.Push(_currentOpacity); _currentOpacity *= opacity; } } @@ -616,14 +624,14 @@ namespace Avalonia.Skia { CheckLease(); - if(_useOpacitySaveLayer) + var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true; + + if (useOpacitySaveLayer) { Canvas.Restore(); } - else - { - _currentOpacity = _opacityStack.Pop(); - } + + _currentOpacity = _opacityStack.Pop(); } /// From 41bef0e991ebd3ffdc7a94725c1d6f66171fb4a5 Mon Sep 17 00:00:00 2001 From: Bill Henning Date: Fri, 18 Aug 2023 11:15:00 -0400 Subject: [PATCH 53/57] Fix for TextBox not accounting for space between ScrollViewer and TextPresenter when calculating MaxLines-based height --- src/Avalonia.Controls/TextBox.cs | 34 +++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 98b3b13c17..8a5ac4e495 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1879,6 +1879,37 @@ namespace Avalonia.Controls return text.Substring(start, end - start); } + /// + /// Returns the sum of any vertical whitespace added between the and in the control template. + /// + /// The total vertical whitespace. + private double GetVerticalSpaceBetweenScrollViewerAndPresenter() + { + var verticalSpace = 0.0; + if (_presenter != null) + { + Visual? visual = _presenter; + while ((visual != null) && (visual != this)) + { + if (visual == _scrollViewer) + { + // ScrollViewer is a stopping point and should only include the Padding + verticalSpace += _scrollViewer.Padding.Top + _scrollViewer.Padding.Bottom; + break; + } + + var margin = visual.GetValue(Layoutable.MarginProperty); + var padding = visual.GetValue(Decorator.PaddingProperty); + + verticalSpace += margin.Top + padding.Top + padding.Bottom + margin.Bottom; + + visual = visual.VisualParent; + } + } + + return verticalSpace; + } + /// /// Raises both the and events. /// @@ -2032,8 +2063,9 @@ namespace Avalonia.Controls var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default); var textLayout = new TextLayout(new MaxLinesTextSource(MaxLines), paragraphProperties); + var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter(); - maxHeight = Math.Ceiling(textLayout.Height); + maxHeight = Math.Ceiling(textLayout.Height + verticalSpace); } _scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight); From 932b40b19b97c4a75ff7b244464b2a6774e93ac4 Mon Sep 17 00:00:00 2001 From: Boyd Patterson Date: Fri, 18 Aug 2023 11:53:42 -0500 Subject: [PATCH 54/57] Added unit tests for TextBox to verify MaxLines property assigns the ScrollViewer.MaxHeight based on line height. --- .../TextBoxTests.cs | 79 ++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index d4558c9e04..3c7fb6f9c0 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -15,6 +15,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Moq; using Xunit; @@ -916,6 +917,82 @@ namespace Avalonia.Controls.UnitTests } } + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void MaxLines_Sets_ScrollViewer_MaxHeight(int maxLines) + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + MaxLines = maxLines, + + // Define explicit whole number line height for predictable calculations + LineHeight = 20 + }; + + var impl = CreateMockTopLevelImpl(); + var topLevel = new TestTopLevel(impl.Object) + { + Template = CreateTopLevelTemplate(), + Content = target + }; + topLevel.ApplyTemplate(); + topLevel.LayoutManager.ExecuteInitialLayoutPass(); + + var textPresenter = target.FindDescendantOfType(); + Assert.Equal("PART_TextPresenter", textPresenter.Name); + Assert.Equal(new Thickness(0), textPresenter.Margin); // Test assumes no margin on TextPresenter + + var scrollViewer = target.FindDescendantOfType(); + Assert.Equal("PART_ScrollViewer", scrollViewer.Name); + Assert.Equal(maxLines * target.LineHeight, scrollViewer.MaxHeight); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void MaxLines_Sets_ScrollViewer_MaxHeight_With_TextPresenter_Margin(int maxLines) + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + MaxLines = maxLines, + + // Define explicit whole number line height for predictable calculations + LineHeight = 20 + }; + + var impl = CreateMockTopLevelImpl(); + var topLevel = new TestTopLevel(impl.Object) + { + Template = CreateTopLevelTemplate(), + Content = target + }; + topLevel.ApplyTemplate(); + topLevel.LayoutManager.ExecuteInitialLayoutPass(); + + var textPresenter = target.FindDescendantOfType(); + Assert.Equal("PART_TextPresenter", textPresenter.Name); + var textPresenterMargin = new Thickness(horizontal: 0, vertical: 3); + textPresenter.Margin = textPresenterMargin; + + target.InvalidateMeasure(); + target.Measure(Size.Infinity); + + var scrollViewer = target.FindDescendantOfType(); + Assert.Equal("PART_ScrollViewer", scrollViewer.Name); + Assert.Equal((maxLines * target.LineHeight) + textPresenterMargin.Top + textPresenterMargin.Bottom, scrollViewer.MaxHeight); + } + } + [Fact] public void CanUndo_CanRedo_Is_False_When_Initialized() { @@ -1125,7 +1202,7 @@ namespace Avalonia.Controls.UnitTests return new FuncControlTemplate((control, scope) => new ScrollViewer { - Name = "Part_ScrollViewer", + Name = "PART_ScrollViewer", Template = new FuncControlTemplate(ScrollViewerTests.CreateTemplate), Content = new TextPresenter { From 3dd62bf533289d0ad00d0ae9bb3ab5fb8eb40763 Mon Sep 17 00:00:00 2001 From: Bill Henning Date: Fri, 18 Aug 2023 14:57:27 -0400 Subject: [PATCH 55/57] Updated ToggleSplitButton.IsCheckedProperty definition to be on a single line. --- src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs index e6f0abcad0..c493445ba1 100644 --- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs @@ -37,8 +37,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IsCheckedProperty = - AvaloniaProperty.Register(nameof(IsChecked), false, - defaultBindingMode: BindingMode.TwoWay); + AvaloniaProperty.Register(nameof(IsChecked), false, defaultBindingMode: BindingMode.TwoWay); /// /// Initializes a new instance of the class. From 795d13101482c6a5b32e11cf4b3a5c13d4797638 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Sat, 19 Aug 2023 13:23:52 +0200 Subject: [PATCH 56/57] Update DrawingContextImpl.cs --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 7b5cca0fde..b1b1de0ffe 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -190,7 +190,7 @@ namespace Avalonia.Skia var paint = SKPaintCache.Shared.Get(); - paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * currentOpacity)); + paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)); paint.FilterQuality = RenderOptions.BitmapInterpolationMode.ToSKFilterQuality(); paint.BlendMode = RenderOptions.BitmapBlendingMode.ToSKBlendMode(); @@ -433,7 +433,7 @@ namespace Avalonia.Skia { if (boxShadow != default && boxShadow.IsInset) { - using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, currentOpacity)) + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) { var spread = (float)boxShadow.Spread; var offsetX = (float)boxShadow.OffsetX; From 15290a3240c18507c28aa4895234476e7ff0fde0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 19 Aug 2023 13:31:47 +0100 Subject: [PATCH 57/57] win32: make setting ShowInTaskbar = true on an owned window that is already shown, not remove the ownership This makes us consistent with WPF. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 154b20ef84..e9156e553d 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -683,9 +683,10 @@ namespace Avalonia.Win32 if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar) { parentHwnd = OffscreenParentWindow.Handle; - _hiddenWindowIsParent = true; } + _hiddenWindowIsParent = parentHwnd == OffscreenParentWindow.Handle; + SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, parentHwnd); }