From 5cf6662f741cb0b9c1bc5cefc4ebe5f058aa8135 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Jul 2020 17:25:15 +0200 Subject: [PATCH 1/9] Bind ListBox.SelectedItems again. Was removed accidentally. --- samples/ControlCatalog/Pages/ListBoxPage.xaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 47b4ce7151..f4d81418ac 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -10,7 +10,13 @@ HorizontalAlignment="Center" Spacing="16"> - + From 73a2637eed83f082be19a7d136aa5a0da164e1aa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 14 Jul 2020 22:04:47 +0200 Subject: [PATCH 2/9] Added failing test for removing selected item with BeginInit. --- .../Primitives/SelectingItemsControlTests.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index fe9c7b1261..9ef2750ff3 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -531,6 +531,7 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); target.SelectedIndex = 1; Assert.Equal(items[1], target.SelectedItem); @@ -549,6 +550,45 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.NotNull(receivedArgs); Assert.Empty(receivedArgs.AddedItems); Assert.Equal(new[] { removed }, receivedArgs.RemovedItems); + Assert.False(items.Single().IsSelected); + } + + [Fact] + public void Removing_Selected_Item_Should_Clear_Selection_With_BeginInit() + { + var items = new AvaloniaList + { + new Item(), + new Item(), + }; + + var target = new SelectingItemsControl(); + target.BeginInit(); + target.Items = items; + target.Template = Template(); + target.EndInit(); + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectedIndex = 0; + + Assert.Equal(items[0], target.SelectedItem); + Assert.Equal(0, target.SelectedIndex); + + SelectionChangedEventArgs receivedArgs = null; + + target.SelectionChanged += (_, args) => receivedArgs = args; + + var removed = items[0]; + + items.RemoveAt(0); + + Assert.Null(target.SelectedItem); + Assert.Equal(-1, target.SelectedIndex); + Assert.NotNull(receivedArgs); + Assert.Empty(receivedArgs.AddedItems); + Assert.Equal(new[] { removed }, receivedArgs.RemovedItems); + Assert.False(items.Single().IsSelected); } [Fact] From 6555a51f5ae6986c17ad2da3f767e467712d612a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 14 Jul 2020 22:37:23 +0200 Subject: [PATCH 3/9] Fix selection after deleting an item. `SelectionModel` needs to subscribe to `CollectionChanged` on the items before `ItemsControl` in order for the selection to be correct when we come to setting the selected state. Because `SelectionModel.Source` isn't subscribed during initialization in `ItemsChanged`, we also need to make sure we don't subscribe `ItemsControl` to the collection changes during initialization. Instead subscribe in `OnInitialized` (this requires a few tests to be rooted in order to be called). Fixes #4293 --- src/Avalonia.Controls/ItemsControl.cs | 11 +++++++++-- tests/Avalonia.Controls.UnitTests/CarouselTests.cs | 3 +++ .../Avalonia.Controls.UnitTests/ItemsControlTests.cs | 4 ++++ .../Primitives/SelectingItemsControlTests.cs | 4 ++++ .../Primitives/SelectingItemsControlTests_Multiple.cs | 4 ++++ 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 6e0ad66699..da9f619932 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -70,7 +70,6 @@ namespace Avalonia.Controls public ItemsControl() { PseudoClasses.Add(":empty"); - SubscribeToItems(_items); } /// @@ -265,6 +264,11 @@ namespace Avalonia.Controls { } + protected override void OnInitialized() + { + SubscribeToItems(_items); + } + /// /// Handles directional navigation within the . /// @@ -330,7 +334,10 @@ namespace Avalonia.Controls Presenter.Items = newValue; } - SubscribeToItems(newValue); + if (IsInitialized) + { + SubscribeToItems(newValue); + } } /// diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index a292910fae..c6ca0fb2bf 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; +using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -155,6 +156,7 @@ namespace Avalonia.Controls.UnitTests IsVirtualized = false }; + var root = new TestRoot(target); target.ApplyTemplate(); target.Presenter.ApplyTemplate(); @@ -247,6 +249,7 @@ namespace Avalonia.Controls.UnitTests IsVirtualized = false }; + var root = new TestRoot(target); target.ApplyTemplate(); target.Presenter.ApplyTemplate(); diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 684486cbae..faaa3ed063 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -131,6 +131,7 @@ namespace Avalonia.Controls.UnitTests var child = new Control(); var items = new AvaloniaList(child); + var root = new TestRoot(target); target.Template = GetTemplate(); target.Items = items; items.RemoveAt(0); @@ -283,6 +284,7 @@ namespace Avalonia.Controls.UnitTests var items = new AvaloniaList { "Foo" }; var called = false; + var root = new TestRoot(target); target.Template = GetTemplate(); target.Items = items; target.ApplyTemplate(); @@ -303,6 +305,7 @@ namespace Avalonia.Controls.UnitTests var items = new AvaloniaList { "Foo", "Bar" }; var called = false; + var root = new TestRoot(target); target.Template = GetTemplate(); target.Items = items; target.ApplyTemplate(); @@ -376,6 +379,7 @@ namespace Avalonia.Controls.UnitTests Items = new[] { 1, 2, 3 }, }; + var root = new TestRoot(target); Assert.DoesNotContain(":empty", target.Classes); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 9ef2750ff3..e43e855ae0 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -170,6 +170,8 @@ namespace Avalonia.Controls.UnitTests.Primitives SelectionMode = SelectionMode.Single | SelectionMode.AlwaysSelected }; + var root = new TestRoot(listBox); + listBox.BeginInit(); listBox.SelectedIndex = 1; @@ -480,6 +482,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Template = Template(), }; + var root = new TestRoot(target); target.ApplyTemplate(); target.Presenter.ApplyTemplate(); items.Add(new Item { IsSelected = true }); @@ -919,6 +922,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Items = items, }; + var root = new TestRoot(target); target.ApplyTemplate(); target.Presenter.ApplyTemplate(); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index dcf25beb50..e9ec8d114f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1014,6 +1014,7 @@ namespace Avalonia.Controls.UnitTests.Primitives SelectionMode = SelectionMode.Multiple, }; + var root = new TestRoot(target); target.ApplyTemplate(); target.Presenter.ApplyTemplate(); @@ -1043,6 +1044,7 @@ namespace Avalonia.Controls.UnitTests.Primitives SelectionMode = SelectionMode.Multiple, }; + var root = new TestRoot(target); target.ApplyTemplate(); target.Presenter.ApplyTemplate(); @@ -1076,6 +1078,7 @@ namespace Avalonia.Controls.UnitTests.Primitives SelectionMode = SelectionMode.Multiple, }; + var root = new TestRoot(target); target.ApplyTemplate(); target.Presenter.ApplyTemplate(); @@ -1199,6 +1202,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Template = Template(), }; + var root = new TestRoot(target); target.ApplyTemplate(); target.Presenter.ApplyTemplate(); items.Add(new ItemContainer { IsSelected = true }); From de87609de1859e1c28fad65af4e233ff5a489bca Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 15 Jul 2020 13:32:06 -0300 Subject: [PATCH 4/9] add failing unit test. --- .../ToolTipTests.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index 34b37e7635..9d7bc6af74 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -30,6 +30,40 @@ namespace Avalonia.Controls.UnitTests Assert.False(ToolTip.GetIsOpen(control)); } + + [Fact] + public void Should_Close_When_Control_Detaches() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + + var panel = new Panel(); + + var target = new Decorator() + { + [ToolTip.TipProperty] = "Tip", + [ToolTip.ShowDelayProperty] = 0 + }; + + panel.Children.Add(target); + + window.Content = panel; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + Assert.True((target as IVisual).IsAttachedToVisualTree); + + _mouseHelper.Enter(target); + + Assert.True(ToolTip.GetIsOpen(target)); + + panel.Children.Remove(target); + + Assert.False(ToolTip.GetIsOpen(target)); + } + } [Fact] public void Should_Open_On_Pointer_Enter() From a542d8753de026bd786c914b571c2a680a2ce77c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 15 Jul 2020 13:33:20 -0300 Subject: [PATCH 5/9] ensure tooltips are closed when its parent detaches from visual tree. --- src/Avalonia.Controls/ToolTipService.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index d90729e8a5..569697304f 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -28,14 +28,22 @@ namespace Avalonia.Controls { control.PointerEnter -= ControlPointerEnter; control.PointerLeave -= ControlPointerLeave; + control.DetachedFromVisualTree -= ControlDetaching; } if (e.NewValue != null) { control.PointerEnter += ControlPointerEnter; control.PointerLeave += ControlPointerLeave; + control.DetachedFromVisualTree += ControlDetaching; } } + + private void ControlDetaching(object sender, VisualTreeAttachmentEventArgs e) + { + var control = (Control)sender; + Close(control); + } /// /// Called when the pointer enters a control with an attached tooltip. From 6e106fb9e7cdd810f7924e0bac04308cd6a803bc Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 15 Jul 2020 22:09:40 +0200 Subject: [PATCH 6/9] [Win32] only steal input focus if it's currently held by Popup's parent's child --- .../Interop/UnmanagedMethods.cs | 12 ++++++++ src/Windows/Avalonia.Win32/PopupImpl.cs | 29 ++++++++++++------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 392ca31282..b7c68c4b95 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1060,9 +1060,21 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool SetFocus(IntPtr hWnd); [DllImport("user32.dll")] + public static extern IntPtr GetFocus(); + [DllImport("user32.dll")] public static extern bool SetParent(IntPtr hWnd, IntPtr hWndNewParent); [DllImport("user32.dll")] public static extern IntPtr GetParent(IntPtr hWnd); + + public enum GetAncestorFlags + { + GA_PARENT = 1, + GA_ROOT = 2, + GA_ROOTOWNER = 3 + } + + [DllImport("user32.dll")] + public static extern IntPtr GetAncestor(IntPtr hwnd, GetAncestorFlags gaFlags); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow); diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index cd25b32ed9..525e5e0d52 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -7,6 +7,7 @@ namespace Avalonia.Win32 { class PopupImpl : WindowImpl, IPopupImpl { + private readonly IWindowBaseImpl _parent; private bool _dropShadowHint = true; private Size? _maxAutoSize; @@ -19,18 +20,25 @@ namespace Avalonia.Win32 public override void Show() { UnmanagedMethods.ShowWindow(Handle.Handle, UnmanagedMethods.ShowWindowCommand.ShowNoActivate); - var parent = UnmanagedMethods.GetParent(Handle.Handle); - if (parent != IntPtr.Zero) - { - IntPtr nextParent = parent; - while (nextParent != IntPtr.Zero) - { - parent = nextParent; - nextParent = UnmanagedMethods.GetParent(parent); - } - UnmanagedMethods.SetFocus(parent); + // We need to steal focus if it's held by a child window of our toplevel window + var parent = _parent; + while(parent != null) + { + if(parent is PopupImpl pi) + parent = pi._parent; + else + break; } + + if(parent == null) + return; + + var focusOwner = UnmanagedMethods.GetFocus(); + if (focusOwner != IntPtr.Zero && + UnmanagedMethods.GetAncestor(focusOwner, UnmanagedMethods.GetAncestorFlags.GA_ROOT) + == parent.Handle.Handle) + UnmanagedMethods.SetFocus(parent.Handle.Handle); } protected override bool ShouldTakeFocusOnClick => false; @@ -118,6 +126,7 @@ namespace Avalonia.Win32 private PopupImpl(IWindowBaseImpl parent, bool dummy) : base() { + _parent = parent; PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); } From 1c099b6e8ca96b60ab8d7a2de4f81c3f79e547c2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 15 Jul 2020 19:15:59 -0300 Subject: [PATCH 7/9] add failing unit tests for cases. --- .../RelativePanelTests.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs b/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs index 7b1f6d07b7..6e171a58e7 100644 --- a/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs @@ -82,5 +82,55 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); Assert.Equal(new Rect(0, 20, 20, 20), target.Children[1].Bounds); } + + [Fact] + public void LeftOf_Measures_Correctly() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Center, + HorizontalAlignment = Layout.HorizontalAlignment.Center, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetLeftOf(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(20, 20), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(-20, 0, 20, 20), target.Children[1].Bounds); + } + + [Fact] + public void Above_Measures_Correctly() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Center, + HorizontalAlignment = Layout.HorizontalAlignment.Center, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetAbove(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(20, 20), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(0, -20, 20, 20), target.Children[1].Bounds); + } } } From 3449784b09d529b950dd3ccd1560a08c5e9703de Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 15 Jul 2020 19:16:53 -0300 Subject: [PATCH 8/9] add fixes for relative panel --- src/Avalonia.Controls/RelativePanel.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/RelativePanel.cs b/src/Avalonia.Controls/RelativePanel.cs index a3ad30db76..38b5d9807c 100644 --- a/src/Avalonia.Controls/RelativePanel.cs +++ b/src/Avalonia.Controls/RelativePanel.cs @@ -54,7 +54,7 @@ namespace Avalonia.Controls } _childGraph.Measure(availableSize); - _childGraph.Reset(); + _childGraph.Reset(false); var boundingSize = _childGraph.GetBoundingSize(Width.IsNaN(), Height.IsNaN()); _childGraph.Reset(); _childGraph.Measure(boundingSize); @@ -119,17 +119,22 @@ namespace Avalonia.Controls public void Arrange(Size arrangeSize) => Element.Arrange(new Rect(Left, Top, Math.Max(arrangeSize.Width - Left - Right, 0), Math.Max(arrangeSize.Height - Top - Bottom, 0))); - public void Reset() + public void Reset(bool clearPos) { - Left = double.NaN; - Top = double.NaN; - Right = double.NaN; - Bottom = double.NaN; + if (clearPos) + { + Left = double.NaN; + Top = double.NaN; + Right = double.NaN; + Bottom = double.NaN; + } + Measured = false; } public Size GetBoundingSize() { + if (Left < 0 || Top < 0) return default; if (Measured) return BoundingSize; @@ -209,7 +214,7 @@ namespace Avalonia.Controls _nodeDic.Clear(); } - public void Reset() => _nodeDic.Values.Do(node => node.Reset()); + public void Reset(bool clearPos = true) => _nodeDic.Values.Do(node => node.Reset(clearPos)); public GraphNode? AddLink(GraphNode from, Layoutable? to) { From d7d63e26c4bc46278815ec56aad5795ed2fdb0d7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 15 Jul 2020 19:17:42 -0300 Subject: [PATCH 9/9] remove chinese comments. --- src/Avalonia.Controls/RelativePanel.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/RelativePanel.cs b/src/Avalonia.Controls/RelativePanel.cs index 38b5d9807c..27f13a3f57 100644 --- a/src/Avalonia.Controls/RelativePanel.cs +++ b/src/Avalonia.Controls/RelativePanel.cs @@ -260,28 +260,21 @@ namespace Avalonia.Controls foreach (var node in nodes) { - /* - * 该节点无任何依赖,所以从这里开始计算元素位置。 - * 因为无任何依赖,所以忽略同级元素 - */ if (!node.Measured && !node.OutgoingNodes.Any()) { MeasureChild(node); continue; } - - // 判断依赖元素是否全部排列完毕 + if (node.OutgoingNodes.All(item => item.Measured)) { MeasureChild(node); continue; } - - // 判断是否有循环 + if (!set.Add(node.Element)) throw new Exception("RelativePanel error: Circular dependency detected. Layout could not complete."); - - // 没有循环,且有依赖,则继续往下 + Measure(node.OutgoingNodes, set); if (!node.Measured)