From b8686877afcbdd2016c622fe77895649a868c607 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 27 Oct 2020 20:29:03 +0200 Subject: [PATCH 01/41] add failing test for issue #4945 --- .../Selection/InternalSelectionModelTests.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs index ce3e698cf3..8701fc2479 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs @@ -232,6 +232,39 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Contains(nameof(target.SelectedItem), changed); } + [Fact] + public void Raises_Selection_Changed_On_Item_Move() + { + var items = new AvaloniaList(new[] { "foo", "bar", "baz" }); + var target = CreateTarget(source: items); + + target.SelectedIndex = 1; + + var changed = new List(); + + target.PropertyChanged += (s, e) => changed.Add(e.PropertyName); + + var oldSelectedIndex = target.SelectedIndex; + var oldSelectedItem = target.SelectedItem; + + + var sel = items[1]; + var other = items[2]; + + items[2] = sel; + items[1] = other; + + Assert.NotEqual(oldSelectedIndex, target.SelectedIndex); + Assert.NotEqual(oldSelectedItem, target.SelectedItem); + + Assert.Equal(-1, target.SelectedIndex); + Assert.Equal(null, target.SelectedItem); + + Assert.Contains(nameof(target.SelectedIndex), changed); + Assert.Contains(nameof(target.SelectedItem), changed); + } + + [Fact] public void Preserves_SelectedItem_On_Items_Reset() { From 53a06aed3b3f23e0ac05822b2047c7bd19179b7d Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 27 Oct 2020 20:40:49 +0200 Subject: [PATCH 02/41] fix notification for selected item in listbox issue #4945 --- src/Avalonia.Controls/Selection/SelectionModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 054974e4f1..6ae53a4d59 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -443,6 +443,7 @@ namespace Avalonia.Controls.Selection } if ((e.Action == NotifyCollectionChangedAction.Remove && e.OldStartingIndex <= oldSelectedIndex) || + (e.Action == NotifyCollectionChangedAction.Replace && e.OldStartingIndex == oldSelectedIndex) || e.Action == NotifyCollectionChangedAction.Reset) { RaisePropertyChanged(nameof(SelectedItem)); From d3b43efe161c3ff12cd8939cff6a821bbc8f94aa Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 29 Oct 2020 11:33:41 +0000 Subject: [PATCH 03/41] if TreeView has a selecteditem that isnt really part of the tree... then do not crash tab navigation. --- src/Avalonia.Controls/TreeView.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index b2bd5ab2e5..f020280496 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -378,10 +378,11 @@ namespace Avalonia.Controls { if (!this.IsVisualAncestorOf(element)) { - IControl result = _selectedItem != null ? + var result = _selectedItem != null ? ItemContainerGenerator.Index.ContainerFromItem(_selectedItem) : ItemContainerGenerator.ContainerFromIndex(0); - return (true, result); + + return (result is {}, result); // SelectedItem may not be in the treeview. } return (true, null); From 27c58c45e15847239b197a89d8407aaa56d390b1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 29 Oct 2020 11:36:08 +0000 Subject: [PATCH 04/41] treeview navigation never return handled... if returning null. --- src/Avalonia.Controls/TreeView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index f020280496..7bd82322d1 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -385,7 +385,7 @@ namespace Avalonia.Controls return (result is {}, result); // SelectedItem may not be in the treeview. } - return (true, null); + return (false, null); } return (false, null); From 3db5e1bc5abacd72e78da9d8f899b2749b7e383a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 29 Oct 2020 12:13:41 +0000 Subject: [PATCH 05/41] Revert "treeview navigation never return handled... if returning null." This reverts commit 27c58c45e15847239b197a89d8407aaa56d390b1. --- src/Avalonia.Controls/TreeView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 7bd82322d1..f020280496 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -385,7 +385,7 @@ namespace Avalonia.Controls return (result is {}, result); // SelectedItem may not be in the treeview. } - return (false, null); + return (true, null); } return (false, null); From d3c44a89c0e5c15ac5ef0e3066fabe8b1fde80ba Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 29 Oct 2020 13:13:15 +0000 Subject: [PATCH 06/41] revert fix and add failing unit test. --- src/Avalonia.Controls/TreeView.cs | 2 +- .../TreeViewTests.cs | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index f020280496..558bcddfbe 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -382,7 +382,7 @@ namespace Avalonia.Controls ItemContainerGenerator.Index.ContainerFromItem(_selectedItem) : ItemContainerGenerator.ContainerFromIndex(0); - return (result is {}, result); // SelectedItem may not be in the treeview. + return (true, result); // SelectedItem may not be in the treeview. } return (true, null); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index b805683393..cea77bb7c9 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -675,6 +675,50 @@ namespace Avalonia.Controls.UnitTests Assert.Same(node, focus.Current); } } + + [Fact] + public void Keyboard_Navigation_Should_Not_Crash_If_Selected_Item_Is_not_In_Tree() + { + using (Application()) + { + var focus = FocusManager.Instance; + var navigation = AvaloniaLocator.Current.GetService(); + var data = CreateTestTreeData(); + + var selectedNode = new Node { Value = "Out of Tree Selected Item" }; + + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = data, + SelectedItem = selectedNode + }; + + var button = new Button(); + + var root = new TestRoot + { + Child = new StackPanel + { + Children = { target, button }, + } + }; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); + + var item = data[0].Children[0]; + var node = target.ItemContainerGenerator.Index.ContainerFromItem(item); + Assert.NotNull(node); + + target.SelectedItem = selectedNode; + node.Focus(); + Assert.Same(node, focus.Current); + + var next = KeyboardNavigationHandler.GetNext(node, NavigationDirection.Previous); + } + } [Fact] public void Pressing_SelectAll_Gesture_Should_Select_All_Nodes() From 22605e52e9d15e9127f308d8e60a70abe38a4770 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 29 Oct 2020 13:14:00 +0000 Subject: [PATCH 07/41] add fix for treeview navigation when SelectedItem is not in the tree. --- src/Avalonia.Controls/TreeView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 558bcddfbe..f020280496 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -382,7 +382,7 @@ namespace Avalonia.Controls ItemContainerGenerator.Index.ContainerFromItem(_selectedItem) : ItemContainerGenerator.ContainerFromIndex(0); - return (true, result); // SelectedItem may not be in the treeview. + return (result is {}, result); // SelectedItem may not be in the treeview. } return (true, null); From a28dc2b9e411714ca8aebbe2e1fc70c94f03471e Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 29 Oct 2020 17:40:27 +0100 Subject: [PATCH 08/41] Throw NotSupportedException inside ContextMenu.Open(null) --- 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 ec9649bc64..31071768c9 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -236,7 +236,7 @@ namespace Avalonia.Controls /// /// Opens the menu. /// - public override void Open() => Open(null); + public override void Open() => throw new NotSupportedException(); /// /// Opens a context menu on the specified control. From 98c2b0f9e0418877fdb28f64684b8831ad6d1f3c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 29 Oct 2020 20:27:36 +0000 Subject: [PATCH 09/41] Initial implementation of IsKeyboardFocusWithin --- src/Avalonia.Input/ApiCompatBaseline.txt | 4 ++ src/Avalonia.Input/IInputElement.cs | 5 ++ src/Avalonia.Input/InputElement.cs | 24 +++++++- src/Avalonia.Input/KeyboardDevice.cs | 71 ++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Input/ApiCompatBaseline.txt diff --git a/src/Avalonia.Input/ApiCompatBaseline.txt b/src/Avalonia.Input/ApiCompatBaseline.txt new file mode 100644 index 0000000000..d960664c1b --- /dev/null +++ b/src/Avalonia.Input/ApiCompatBaseline.txt @@ -0,0 +1,4 @@ +Compat issues with assembly Avalonia.Input: +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Input.IInputElement.IsKeyboardFocusWithin' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Input.IInputElement.IsKeyboardFocusWithin.get()' is present in the implementation but not in the contract. +Total Issues: 2 diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs index 12fec82368..7aa9c32bca 100644 --- a/src/Avalonia.Input/IInputElement.cs +++ b/src/Avalonia.Input/IInputElement.cs @@ -89,6 +89,11 @@ namespace Avalonia.Input /// value of this control and its parent controls. /// bool IsEffectivelyEnabled { get; } + + /// + /// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements. + /// + bool IsKeyboardFocusWithin { get; } /// /// Gets a value indicating whether the control is focused. diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 9ace7fd92d..1db7db6ba2 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -42,6 +42,14 @@ namespace Avalonia.Input public static readonly StyledProperty CursorProperty = AvaloniaProperty.Register(nameof(Cursor), null, true); + /// + /// Defines the property. + /// + public static readonly DirectProperty IsKeyboardFocusWithinProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsKeyboardFocusWithin), + o => o.IsKeyboardFocusWithin); + /// /// Defines the property. /// @@ -160,6 +168,7 @@ namespace Avalonia.Input private bool _isEffectivelyEnabled = true; private bool _isFocused; + private bool _isKeyboardFocusWithin; private bool _isFocusVisible; private bool _isPointerOver; private GestureRecognizerCollection? _gestureRecognizers; @@ -343,6 +352,15 @@ namespace Avalonia.Input get { return GetValue(CursorProperty); } set { SetValue(CursorProperty, value); } } + + /// + /// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements. + /// + public bool IsKeyboardFocusWithin + { + get => _isKeyboardFocusWithin; + internal set => SetAndRaise(IsKeyboardFocusWithinProperty, ref _isKeyboardFocusWithin, value); + } /// /// Gets a value indicating whether the control is focused. @@ -423,7 +441,7 @@ namespace Avalonia.Input base.OnAttachedToVisualTreeCore(e); UpdateIsEffectivelyEnabled(); } - + /// /// Called before the event occurs. /// @@ -544,6 +562,10 @@ namespace Avalonia.Input { UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault()); } + else if (change.Property == IsKeyboardFocusWithinProperty) + { + PseudoClasses.Set(":focus-within", _isKeyboardFocusWithin); + } } /// diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 187670a26b..758a398f3a 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -31,6 +31,74 @@ namespace Avalonia.Input RaisePropertyChanged(); } } + + private void ClearFocusWithin(IInputElement element, bool clearRoot) + { + foreach (IInputElement el in element.VisualChildren) + { + if (el.IsKeyboardFocusWithin) + { + ClearFocusWithin(el, true); + break; + } + } + + if(clearRoot) + { + if (element is InputElement ie) + { + ie.IsKeyboardFocusWithin = false; + } + } + } + + private void SetIsFocusWithin(InputElement oldElement, InputElement newElement) + { + InputElement? branch = null; + + InputElement el = newElement; + + while (el != null) + { + if (el.IsKeyboardFocusWithin) + { + branch = el; + break; + } + + if ((el as IInputElement).VisualParent is InputElement ie) + { + el = ie; + } + else + { + break; + } + } + + el = oldElement; + + if (el != null && branch != null) + { + ClearFocusWithin(branch, false); + } + + el = newElement; + + while (el != null && el != branch) + { + el.IsKeyboardFocusWithin = true; + + if ((el as IInputElement).VisualParent is InputElement ie) + { + el = ie; + } + else + { + break; + } + } + } public void SetFocusedElement( IInputElement? element, @@ -40,6 +108,9 @@ namespace Avalonia.Input if (element != FocusedElement) { var interactive = FocusedElement as IInteractive; + + SetIsFocusWithin(FocusedElement as InputElement, element as InputElement); + FocusedElement = element; interactive?.RaiseEvent(new RoutedEventArgs From 7cae6af637aff6ab3ee180049911d2a1df01ba44 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 29 Oct 2020 21:24:43 +0000 Subject: [PATCH 10/41] less casting. --- src/Avalonia.Input/KeyboardDevice.cs | 31 +++++++++------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 758a398f3a..9653b78e2d 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -52,11 +52,11 @@ namespace Avalonia.Input } } - private void SetIsFocusWithin(InputElement oldElement, InputElement newElement) + private void SetIsFocusWithin(IInputElement oldElement, IInputElement newElement) { - InputElement? branch = null; + IInputElement? branch = null; - InputElement el = newElement; + IInputElement el = newElement; while (el != null) { @@ -66,14 +66,7 @@ namespace Avalonia.Input break; } - if ((el as IInputElement).VisualParent is InputElement ie) - { - el = ie; - } - else - { - break; - } + el = (IInputElement)el.VisualParent; } el = oldElement; @@ -87,16 +80,12 @@ namespace Avalonia.Input while (el != null && el != branch) { - el.IsKeyboardFocusWithin = true; - - if ((el as IInputElement).VisualParent is InputElement ie) + if (el is InputElement ie) { - el = ie; - } - else - { - break; + ie.IsKeyboardFocusWithin = true; } + + el = (IInputElement)el.VisualParent; } } @@ -108,8 +97,8 @@ namespace Avalonia.Input if (element != FocusedElement) { var interactive = FocusedElement as IInteractive; - - SetIsFocusWithin(FocusedElement as InputElement, element as InputElement); + + SetIsFocusWithin(FocusedElement, element); FocusedElement = element; From 6b324eb05839731dddd527a1e219951474e5fe68 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 29 Oct 2020 21:31:12 +0000 Subject: [PATCH 11/41] update reactiveui --- build/ReactiveUI.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props index d8e86e917e..f74ab07e31 100644 --- a/build/ReactiveUI.props +++ b/build/ReactiveUI.props @@ -1,5 +1,5 @@ - + From 50b2b3320c8625fb1a14e3bdc272fb833f4c9d3b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 30 Oct 2020 15:17:19 +0100 Subject: [PATCH 12/41] Move test to existing test case. --- .../Selection/InternalSelectionModelTests.cs | 33 ------------------- .../Selection/SelectionModelTests_Multiple.cs | 7 ++++ .../Selection/SelectionModelTests_Single.cs | 7 ++++ 3 files changed, 14 insertions(+), 33 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs index 8701fc2479..ce3e698cf3 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/InternalSelectionModelTests.cs @@ -232,39 +232,6 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Contains(nameof(target.SelectedItem), changed); } - [Fact] - public void Raises_Selection_Changed_On_Item_Move() - { - var items = new AvaloniaList(new[] { "foo", "bar", "baz" }); - var target = CreateTarget(source: items); - - target.SelectedIndex = 1; - - var changed = new List(); - - target.PropertyChanged += (s, e) => changed.Add(e.PropertyName); - - var oldSelectedIndex = target.SelectedIndex; - var oldSelectedItem = target.SelectedItem; - - - var sel = items[1]; - var other = items[2]; - - items[2] = sel; - items[1] = other; - - Assert.NotEqual(oldSelectedIndex, target.SelectedIndex); - Assert.NotEqual(oldSelectedItem, target.SelectedItem); - - Assert.Equal(-1, target.SelectedIndex); - Assert.Equal(null, target.SelectedItem); - - Assert.Contains(nameof(target.SelectedIndex), changed); - Assert.Contains(nameof(target.SelectedItem), changed); - } - - [Fact] public void Preserves_SelectedItem_On_Items_Reset() { diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs index 5d0c6d31e1..68bdbe51e8 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs @@ -1216,6 +1216,7 @@ namespace Avalonia.Controls.UnitTests.Selection var data = (AvaloniaList)target.Source!; var selectionChangedRaised = 0; var selectedIndexRaised = 0; + var selectedItemRaised = 0; var indexesChangedRaised = 0; target.Source = data; @@ -1227,6 +1228,11 @@ namespace Avalonia.Controls.UnitTests.Selection { ++selectedIndexRaised; } + + if (e.PropertyName == nameof(target.SelectedItem)) + { + ++selectedItemRaised; + } }; target.IndexesChanged += (s, e) => ++indexesChangedRaised; @@ -1249,6 +1255,7 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(2, target.AnchorIndex); Assert.Equal(1, selectionChangedRaised); Assert.Equal(1, selectedIndexRaised); + Assert.Equal(1, selectedItemRaised); Assert.Equal(0, indexesChangedRaised); } diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs index 66a2cef921..668af3b5d7 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs @@ -1040,6 +1040,7 @@ namespace Avalonia.Controls.UnitTests.Selection var data = (AvaloniaList)target.Source!; var selectionChangedRaised = 0; var selectedIndexRaised = 0; + var selectedItemRaised = 0; target.Source = data; target.Select(1); @@ -1050,6 +1051,11 @@ namespace Avalonia.Controls.UnitTests.Selection { ++selectedIndexRaised; } + + if (e.PropertyName == nameof(target.SelectedItem)) + { + ++selectedItemRaised; + } }; target.SelectionChanged += (s, e) => @@ -1070,6 +1076,7 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(-1, target.AnchorIndex); Assert.Equal(1, selectionChangedRaised); Assert.Equal(1, selectedIndexRaised); + Assert.Equal(1, selectedItemRaised); } [Fact] From 7a33cd4e825f70dbc1fb4cef7dabb63d98b1f009 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 2 Nov 2020 10:22:55 +0000 Subject: [PATCH 13/41] change syntax. --- src/Avalonia.Controls/TreeView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index f020280496..09742412d9 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -382,7 +382,7 @@ namespace Avalonia.Controls ItemContainerGenerator.Index.ContainerFromItem(_selectedItem) : ItemContainerGenerator.ContainerFromIndex(0); - return (result is {}, result); // SelectedItem may not be in the treeview. + return (result != null, result); // SelectedItem may not be in the treeview. } return (true, null); From 1cc569472ef90b7940ec8530e15a2eb7e1700341 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 2 Nov 2020 11:45:11 +0100 Subject: [PATCH 14/41] Fix ncrunch. --- .ncrunch/Avalonia.Build.Tasks.v3.ncrunchproject | 4 +--- .ncrunch/Avalonia.MicroCom.v3.ncrunchproject | 5 +++++ Avalonia.v3.ncrunchsolution | 2 ++ 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .ncrunch/Avalonia.MicroCom.v3.ncrunchproject diff --git a/.ncrunch/Avalonia.Build.Tasks.v3.ncrunchproject b/.ncrunch/Avalonia.Build.Tasks.v3.ncrunchproject index 319cd523ce..95a483b433 100644 --- a/.ncrunch/Avalonia.Build.Tasks.v3.ncrunchproject +++ b/.ncrunch/Avalonia.Build.Tasks.v3.ncrunchproject @@ -1,5 +1,3 @@  - - True - + \ No newline at end of file diff --git a/.ncrunch/Avalonia.MicroCom.v3.ncrunchproject b/.ncrunch/Avalonia.MicroCom.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Avalonia.MicroCom.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.v3.ncrunchsolution b/Avalonia.v3.ncrunchsolution index afce1018ec..b97a8e54f5 100644 --- a/Avalonia.v3.ncrunchsolution +++ b/Avalonia.v3.ncrunchsolution @@ -4,6 +4,8 @@ tests\TestFiles\**.* src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Avalonia.Build.Tasks.dll src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll + src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.Rocks.dll + src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.Pdb.dll True From deb0a58eb48e7f8eb4e0f401809fdefe8a78f327 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 2 Nov 2020 11:07:01 +0000 Subject: [PATCH 15/41] Add some unit tests for FocusWithin --- .../InputElement_Focus.cs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs b/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs index 09fae7207f..7e1f7f1dc3 100644 --- a/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs +++ b/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs @@ -121,5 +121,93 @@ namespace Avalonia.Input.UnitTests Assert.False(target2.Classes.Contains(":focus-visible")); } } + + [Fact] + public void Control_FocusWithin_PseudoClass_Should_Be_Applied() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var target1 = new Decorator(); + var target2 = new Decorator(); + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + target1, + target2 + } + } + }; + + target1.ApplyTemplate(); + target2.ApplyTemplate(); + + FocusManager.Instance?.Focus(target1); + Assert.True(target1.IsFocused); + Assert.True(target1.Classes.Contains(":focus-within")); + Assert.True(target1.IsKeyboardFocusWithin); + Assert.True(root.Child.Classes.Contains(":focus-within")); + Assert.True(root.Child.IsKeyboardFocusWithin); + Assert.True(root.Classes.Contains(":focus-within")); + Assert.True(root.IsKeyboardFocusWithin); + } + } + + [Fact] + public void Control_FocusWithin_PseudoClass_Should_Be_Applied_and_Removed() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var target1 = new Decorator(); + var target2 = new Decorator(); + var panel1 = new Panel { Children = { target1 } }; + var panel2 = new Panel { Children = { target2 } }; + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + panel1, + panel2 + } + } + }; + + target1.ApplyTemplate(); + target2.ApplyTemplate(); + + FocusManager.Instance?.Focus(target1); + Assert.True(target1.IsFocused); + Assert.True(target1.Classes.Contains(":focus-within")); + Assert.True(target1.IsKeyboardFocusWithin); + Assert.True(panel1.Classes.Contains(":focus-within")); + Assert.True(panel1.IsKeyboardFocusWithin); + Assert.True(root.Child.Classes.Contains(":focus-within")); + Assert.True(root.Child.IsKeyboardFocusWithin); + Assert.True(root.Classes.Contains(":focus-within")); + Assert.True(root.IsKeyboardFocusWithin); + + FocusManager.Instance?.Focus(target2); + + Assert.False(target1.IsFocused); + Assert.False(target1.Classes.Contains(":focus-within")); + Assert.False(target1.IsKeyboardFocusWithin); + Assert.False(panel1.Classes.Contains(":focus-within")); + Assert.False(panel1.IsKeyboardFocusWithin); + Assert.True(root.Child.Classes.Contains(":focus-within")); + Assert.True(root.Child.IsKeyboardFocusWithin); + Assert.True(root.Classes.Contains(":focus-within")); + Assert.True(root.IsKeyboardFocusWithin); + + Assert.True(target2.IsFocused); + Assert.True(target2.Classes.Contains(":focus-within")); + Assert.True(target2.IsKeyboardFocusWithin); + Assert.True(panel2.Classes.Contains(":focus-within")); + Assert.True(panel2.IsKeyboardFocusWithin); + } + } } } From f9f4e73bd3f5691e3fa5b99baff125ecb7934aab Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 2 Nov 2020 11:09:07 +0000 Subject: [PATCH 16/41] whitespace. --- src/Avalonia.Input/InputElement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 1db7db6ba2..66fb9cfb1c 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -441,7 +441,7 @@ namespace Avalonia.Input base.OnAttachedToVisualTreeCore(e); UpdateIsEffectivelyEnabled(); } - + /// /// Called before the event occurs. /// From 8e2d55523922a5b03a388244496f6dc5587663d0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 2 Nov 2020 11:48:27 +0000 Subject: [PATCH 17/41] focus within copes with detach from tree. --- src/Avalonia.Input/KeyboardDevice.cs | 57 ++++++++++++++++++- .../InputElement_Focus.cs | 45 +++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 9653b78e2d..43a44331a7 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -9,6 +9,7 @@ namespace Avalonia.Input public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged { private IInputElement? _focusedElement; + private IInputRoot? _focusedRoot; public event PropertyChangedEventHandler? PropertyChanged; @@ -28,9 +29,34 @@ namespace Avalonia.Input private set { _focusedElement = value; + + if (_focusedElement != null && _focusedElement.IsAttachedToVisualTree) + { + _focusedRoot = _focusedElement.VisualRoot as IInputRoot; + } + else + { + _focusedRoot = null; + } + RaisePropertyChanged(); } } + + private void ClearFocusWithinAncestors(IInputElement element) + { + IInputElement el = element; + + while (el != null) + { + if (el is InputElement ie) + { + ie.IsKeyboardFocusWithin = false; + } + + el = (IInputElement)el.VisualParent; + } + } private void ClearFocusWithin(IInputElement element, bool clearRoot) { @@ -54,9 +80,15 @@ namespace Avalonia.Input private void SetIsFocusWithin(IInputElement oldElement, IInputElement newElement) { + if (newElement == null && oldElement != null) + { + ClearFocusWithinAncestors(oldElement); + return; + } + IInputElement? branch = null; - IInputElement el = newElement; + IInputElement? el = newElement; while (el != null) { @@ -69,7 +101,7 @@ namespace Avalonia.Input el = (IInputElement)el.VisualParent; } - el = oldElement; + el = oldElement!; if (el != null && branch != null) { @@ -88,6 +120,22 @@ namespace Avalonia.Input el = (IInputElement)el.VisualParent; } } + + private void ClearChildrenFocusWithin(IInputElement element,bool clearRoot) + { + foreach (IInputElement el in element.VisualChildren) + { + if (el.IsKeyboardFocusWithin) + { + ClearChildrenFocusWithin(el, true); + break; + } + } + if(clearRoot && element is InputElement ie) + { + ie.IsKeyboardFocusWithin = false; + } + } public void SetFocusedElement( IInputElement? element, @@ -98,6 +146,11 @@ namespace Avalonia.Input { var interactive = FocusedElement as IInteractive; + if (FocusedElement != null && !FocusedElement.IsAttachedToVisualTree && _focusedRoot != null) + { + ClearChildrenFocusWithin(_focusedRoot, true); + } + SetIsFocusWithin(FocusedElement, element); FocusedElement = element; diff --git a/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs b/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs index 7e1f7f1dc3..b79ae92ef3 100644 --- a/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs +++ b/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs @@ -209,5 +209,50 @@ namespace Avalonia.Input.UnitTests Assert.True(panel2.IsKeyboardFocusWithin); } } + + [Fact] + public void Control_FocusVsisible_Pseudoclass_Should_Be_Removed_When_Removed_From_Tree() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var target1 = new Decorator(); + var target2 = new Decorator(); + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + target1, + target2 + } + } + }; + + target1.ApplyTemplate(); + target2.ApplyTemplate(); + + FocusManager.Instance?.Focus(target1); + Assert.True(target1.IsFocused); + Assert.True(target1.Classes.Contains(":focus-within")); + Assert.True(target1.IsKeyboardFocusWithin); + Assert.True(root.Child.Classes.Contains(":focus-within")); + Assert.True(root.Child.IsKeyboardFocusWithin); + Assert.True(root.Classes.Contains(":focus-within")); + Assert.True(root.IsKeyboardFocusWithin); + + Assert.Equal(KeyboardDevice.Instance.FocusedElement, target1); + + root.Child = null; + + Assert.Null(KeyboardDevice.Instance.FocusedElement); + + Assert.False(target1.IsFocused); + Assert.False(target1.Classes.Contains(":focus-within")); + Assert.False(target1.IsKeyboardFocusWithin); + Assert.False(root.Classes.Contains(":focus-within")); + Assert.False(root.IsKeyboardFocusWithin); + } + } } } From 2baae49a12e55fa4fe20d993d00e95f42bcc7501 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 2 Nov 2020 17:28:51 +0000 Subject: [PATCH 18/41] fix nullable reference type warnings. --- src/Avalonia.Input/KeyboardDevice.cs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 43a44331a7..099fc74766 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -43,9 +43,9 @@ namespace Avalonia.Input } } - private void ClearFocusWithinAncestors(IInputElement element) + private void ClearFocusWithinAncestors(IInputElement? element) { - IInputElement el = element; + var el = element; while (el != null) { @@ -60,16 +60,16 @@ namespace Avalonia.Input private void ClearFocusWithin(IInputElement element, bool clearRoot) { - foreach (IInputElement el in element.VisualChildren) + foreach (var visual in element.VisualChildren) { - if (el.IsKeyboardFocusWithin) + if (visual is IInputElement el && el.IsKeyboardFocusWithin) { ClearFocusWithin(el, true); break; } } - if(clearRoot) + if (clearRoot) { if (element is InputElement ie) { @@ -78,7 +78,7 @@ namespace Avalonia.Input } } - private void SetIsFocusWithin(IInputElement oldElement, IInputElement newElement) + private void SetIsFocusWithin(IInputElement? oldElement, IInputElement? newElement) { if (newElement == null && oldElement != null) { @@ -88,7 +88,7 @@ namespace Avalonia.Input IInputElement? branch = null; - IInputElement? el = newElement; + var el = newElement; while (el != null) { @@ -101,7 +101,7 @@ namespace Avalonia.Input el = (IInputElement)el.VisualParent; } - el = oldElement!; + el = oldElement; if (el != null && branch != null) { @@ -121,17 +121,18 @@ namespace Avalonia.Input } } - private void ClearChildrenFocusWithin(IInputElement element,bool clearRoot) + private void ClearChildrenFocusWithin(IInputElement element, bool clearRoot) { - foreach (IInputElement el in element.VisualChildren) + foreach (var visual in element.VisualChildren) { - if (el.IsKeyboardFocusWithin) + if (visual is IInputElement el && el.IsKeyboardFocusWithin) { ClearChildrenFocusWithin(el, true); break; } } - if(clearRoot && element is InputElement ie) + + if (clearRoot && element is InputElement ie) { ie.IsKeyboardFocusWithin = false; } From eeba3f0e2e7ee123783617b41bd7f64a772c5c0d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 2 Nov 2020 20:39:34 +0300 Subject: [PATCH 19/41] AddSyntheticProjectReferencesForSolutionDependencies = false --- Directory.Build.props | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Directory.Build.props b/Directory.Build.props index b41f8c488e..c6610695c4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,5 +2,7 @@ $(MSBuildThisFileDirectory)build-intermediate/nuget $(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\netcoreapp2.0\Avalonia.Designer.HostApp.dll + + false From 7354ba337a3b7883878735f4eb21995b7e53760a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 2 Nov 2020 18:50:27 +0100 Subject: [PATCH 20/41] Use as instead of cast. In case someone's doing something funky, fail gracefully. --- src/Avalonia.Input/KeyboardDevice.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 099fc74766..1a161ae56a 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -98,7 +98,7 @@ namespace Avalonia.Input break; } - el = (IInputElement)el.VisualParent; + el = el.VisualParent as IInputElement; } el = oldElement; @@ -117,8 +117,8 @@ namespace Avalonia.Input ie.IsKeyboardFocusWithin = true; } - el = (IInputElement)el.VisualParent; - } + el = el.VisualParent as IInputElement; + } } private void ClearChildrenFocusWithin(IInputElement element, bool clearRoot) From 72c3f6443dc3cd70d4f7687afbfea0dc2450401b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 2 Nov 2020 20:54:30 +0300 Subject: [PATCH 21/41] microcom? --- src/Avalonia.MicroCom/Avalonia.MicroCom.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj index 6af3b4347a..b796e173c4 100644 --- a/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj +++ b/src/Avalonia.MicroCom/Avalonia.MicroCom.csproj @@ -10,7 +10,6 @@ false all - true From 8a863a1c1114099b5ea88aadd2b62f15759aca7e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 2 Nov 2020 21:34:45 +0300 Subject: [PATCH 22/41] Update build.md --- Documentation/build.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Documentation/build.md b/Documentation/build.md index 8c2ef57b54..2f59146a48 100644 --- a/Documentation/build.md +++ b/Documentation/build.md @@ -30,7 +30,7 @@ The build process needs [Xcode](https://developer.apple.com/xcode/) to build the Linux operating systems ship with their own respective package managers however we will use [Homebrew](https://brew.sh/) to manage packages on macOS. To install follow the instructions [here](https://docs.brew.sh/Installation). -### Install CastXML +### Install CastXML (pre Nov 2020) Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processing during the build process. The easiest way to install this is via the operating system's package managers, such as below. @@ -61,6 +61,7 @@ git submodule update --init --recursive ### Build native libraries (macOS only) On macOS it is necessary to build and manually install the respective native libraries using [Xcode](https://developer.apple.com/xcode/). The steps to get this working correctly are: +- (for revisions after 2 Nov 2020) Run `./build.sh GenerateCppHeaders` to generate `avalonia-native.h` from `avn.idl` - Navigate to the Avalonia/native/Avalonia.Native/src/OSX folder and open the `Avalonia.Native.OSX.xcodeproj` project - Build the library via the Product->Build menu. This will generate binaries in your local path under ~/Library/Developer/Xcode/DerivedData/Avalonia.Native.OSX-*guid* where "guid" is uniquely generated every time you build. - Manually install the native library by copying it from the build artifacts folder into the shared dynamic library path: From 81e6df9188d47a2debadadfec216b9434c49c2ab Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 2 Nov 2020 21:36:10 +0300 Subject: [PATCH 23/41] CompileNative should depend on GenerateCppHeaders --- nukebuild/Build.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index ecc01b4aab..97647a1c59 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -141,6 +141,7 @@ partial class Build : NukeBuild Target CompileNative => _ => _ .DependsOn(Clean) + .DependsOn(GenerateCppHeaders) .OnlyWhenStatic(() => EnvironmentInfo.IsOsx) .Executes(() => { From 9dee53c7d954a39fa5b3adcfdee00f1ddf0c3d18 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 2 Nov 2020 18:58:15 +0000 Subject: [PATCH 24/41] add failing unit test for focus within tranfer of focus between roots. --- .../InputElement_Focus.cs | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs b/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs index b79ae92ef3..8b8f2fa775 100644 --- a/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs +++ b/tests/Avalonia.Input.UnitTests/InputElement_Focus.cs @@ -211,7 +211,7 @@ namespace Avalonia.Input.UnitTests } [Fact] - public void Control_FocusVsisible_Pseudoclass_Should_Be_Removed_When_Removed_From_Tree() + public void Control_FocusWithin_Pseudoclass_Should_Be_Removed_When_Removed_From_Tree() { using (UnitTestApplication.Start(TestServices.RealFocus)) { @@ -254,5 +254,69 @@ namespace Avalonia.Input.UnitTests Assert.False(root.IsKeyboardFocusWithin); } } + + [Fact] + public void Control_FocusWithin_Pseudoclass_Should_Be_Removed_Focus_Moves_To_Different_Root() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var target1 = new Decorator(); + var target2 = new Decorator(); + + var root1 = new TestRoot + { + Child = new StackPanel + { + Children = + { + target1, + } + } + }; + + var root2 = new TestRoot + { + Child = new StackPanel + { + Children = + { + target2, + } + } + }; + + target1.ApplyTemplate(); + target2.ApplyTemplate(); + + FocusManager.Instance?.Focus(target1); + Assert.True(target1.IsFocused); + Assert.True(target1.Classes.Contains(":focus-within")); + Assert.True(target1.IsKeyboardFocusWithin); + Assert.True(root1.Child.Classes.Contains(":focus-within")); + Assert.True(root1.Child.IsKeyboardFocusWithin); + Assert.True(root1.Classes.Contains(":focus-within")); + Assert.True(root1.IsKeyboardFocusWithin); + + Assert.Equal(KeyboardDevice.Instance.FocusedElement, target1); + + FocusManager.Instance?.Focus(target2); + + Assert.False(target1.IsFocused); + Assert.False(target1.Classes.Contains(":focus-within")); + Assert.False(target1.IsKeyboardFocusWithin); + Assert.False(root1.Child.Classes.Contains(":focus-within")); + Assert.False(root1.Child.IsKeyboardFocusWithin); + Assert.False(root1.Classes.Contains(":focus-within")); + Assert.False(root1.IsKeyboardFocusWithin); + + Assert.True(target2.IsFocused); + Assert.True(target2.Classes.Contains(":focus-within")); + Assert.True(target2.IsKeyboardFocusWithin); + Assert.True(root2.Child.Classes.Contains(":focus-within")); + Assert.True(root2.Child.IsKeyboardFocusWithin); + Assert.True(root2.Classes.Contains(":focus-within")); + Assert.True(root2.IsKeyboardFocusWithin); + } + } } } From d36b7a40df223b7a3805dfb57dae9e01392fcfd1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 2 Nov 2020 19:02:54 +0000 Subject: [PATCH 25/41] ensure focus-within is removed when focus moves to a different root. --- src/Avalonia.Input/KeyboardDevice.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 1a161ae56a..6f4cb7a35c 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -147,7 +147,10 @@ namespace Avalonia.Input { var interactive = FocusedElement as IInteractive; - if (FocusedElement != null && !FocusedElement.IsAttachedToVisualTree && _focusedRoot != null) + if (FocusedElement != null && + (!FocusedElement.IsAttachedToVisualTree || + _focusedRoot != element?.VisualRoot as IInputRoot) && + _focusedRoot != null) { ClearChildrenFocusWithin(_focusedRoot, true); } From 5be046e5efa1a8c9b33a25cec8a4c6530a852123 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Tue, 3 Nov 2020 07:37:52 +0100 Subject: [PATCH 26/41] Fix TextDecoration location calculation --- src/Avalonia.Visuals/Media/TextDecoration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Media/TextDecoration.cs b/src/Avalonia.Visuals/Media/TextDecoration.cs index d9b3f664ce..57936426f3 100644 --- a/src/Avalonia.Visuals/Media/TextDecoration.cs +++ b/src/Avalonia.Visuals/Media/TextDecoration.cs @@ -189,11 +189,11 @@ namespace Avalonia.Media break; case TextDecorationLocation.Strikethrough: origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X, - shapedTextCharacters.GlyphRun.BaselineOrigin.Y - fontMetrics.StrikethroughPosition); + shapedTextCharacters.GlyphRun.BaselineOrigin.Y + fontMetrics.StrikethroughPosition); break; case TextDecorationLocation.Underline: origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X, - shapedTextCharacters.GlyphRun.BaselineOrigin.Y - fontMetrics.UnderlinePosition); + shapedTextCharacters.GlyphRun.BaselineOrigin.Y + fontMetrics.UnderlinePosition); break; } From 760f13f0934f3192b147645606e5a72894ee3caf Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 4 Nov 2020 14:54:18 +0100 Subject: [PATCH 27/41] fixed null reference exception when using MultiBinding without StringFormat and Converter --- src/Markup/Avalonia.Markup/Data/MultiBinding.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 7aa1eed890..8b31f7b560 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -116,7 +116,8 @@ namespace Avalonia.Data } var culture = CultureInfo.CurrentCulture; - var converted = converter.Convert(values, targetType, ConverterParameter, culture); + var converted = converter?.Convert(values, targetType, ConverterParameter, culture) + ?? values.ToArray(); if (converted == null) { From 411b2278340dffa337715996ed9cc4f9a891ec96 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 4 Nov 2020 15:29:22 +0100 Subject: [PATCH 28/41] Adding test --- .../Data/MultiBindingTests.cs | 23 +++++++++++++++++++ .../Extensions/IEnummerableExtension.cs | 21 +++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tests/Avalonia.Markup.UnitTests/Extensions/IEnummerableExtension.cs diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index 64f5eb2a0e..b87922aa1e 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -157,6 +157,29 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("1,2,Fallback", target.Text); } + [Fact] + public void MultiBinding_Without_StringFormat_And_Converter() + { + var source = new { A = 1, B = 2, C = 3 }; + var target = new ItemsControl { }; + + var binding = new MultiBinding + { + Bindings = new[] + { + new Binding { Path = "A", Source = source }, + new Binding { Path = "B", Source = source }, + new Binding { Path = "C", Source = source }, + }, + }; + + target.Bind(ItemsControl.ItemsProperty, binding); + Assert.Equal(target.ItemCount, 3); + Assert.Equal(target.Items.ElementAt(0), source.A); + Assert.Equal(target.Items.ElementAt(1), source.B); + Assert.Equal(target.Items.ElementAt(2), source.C); + } + private class ConcatConverter : IMultiValueConverter { public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) diff --git a/tests/Avalonia.Markup.UnitTests/Extensions/IEnummerableExtension.cs b/tests/Avalonia.Markup.UnitTests/Extensions/IEnummerableExtension.cs new file mode 100644 index 0000000000..33be06baed --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Extensions/IEnummerableExtension.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections; + +namespace Avalonia.Markup.UnitTests +{ + static class IEnummerableExtension + { + public static object ElementAt(this IEnumerable source, int index) + { + var i = -1; + var enumerator = source.GetEnumerator(); + + while (enumerator.MoveNext() && ++i < index); + if (i == index) + { + return enumerator.Current; + } + throw new ArgumentOutOfRangeException(nameof(index)); + } + } +} From 8ed53d47beb7194bfa8bb6a1df30f8b1bb2c0530 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 4 Nov 2020 16:14:29 +0100 Subject: [PATCH 29/41] fixes Should_Return_TargetNullValue_When_Value_Is_Null fails --- src/Markup/Avalonia.Markup/Data/MultiBinding.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 8b31f7b560..5bdb424f68 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -48,7 +48,7 @@ namespace Avalonia.Data /// Gets or sets the binding priority. /// public BindingPriority Priority { get; set; } - + /// /// Gets or sets the relative source for the binding. /// @@ -77,12 +77,12 @@ namespace Avalonia.Data // We only respect `StringFormat` if the type of the property we're assigning to will // accept a string. Note that this is slightly different to WPF in that WPF only applies // `StringFormat` for target type `string` (not `object`). - if (!string.IsNullOrWhiteSpace(StringFormat) && + if (!string.IsNullOrWhiteSpace(StringFormat) && (targetType == typeof(string) || targetType == typeof(object))) { converter = new StringFormatMultiValueConverter(StringFormat, converter); } - + var children = Bindings.Select(x => x.Initiate(target, null)); var input = children.Select(x => x.Observable) @@ -116,8 +116,13 @@ namespace Avalonia.Data } var culture = CultureInfo.CurrentCulture; - var converted = converter?.Convert(values, targetType, ConverterParameter, culture) - ?? values.ToArray(); + object converted; + if (converter != null) + { + converted = converter.Convert(values, targetType, ConverterParameter, culture); + } + else + converted = values.ToArray(); if (converted == null) { From dbd0e3648e024719fe080d43a1c66e40b08eb320 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 5 Nov 2020 10:06:20 +0100 Subject: [PATCH 30/41] fix code smell --- src/Markup/Avalonia.Markup/Data/MultiBinding.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 5bdb424f68..a48ac74f29 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -122,7 +122,9 @@ namespace Avalonia.Data converted = converter.Convert(values, targetType, ConverterParameter, culture); } else + { converted = values.ToArray(); + } if (converted == null) { From dd1941e6ec7d7bbf8af6fa429ebf5c72195765cf Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 5 Nov 2020 10:20:48 +0100 Subject: [PATCH 31/41] used ReadOnlyCollection instead of ToArray () --- src/Markup/Avalonia.Markup/Data/MultiBinding.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index a48ac74f29..cbc5f414f2 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -116,6 +116,7 @@ namespace Avalonia.Data } var culture = CultureInfo.CurrentCulture; + values = new System.Collections.ObjectModel.ReadOnlyCollection(values); object converted; if (converter != null) { @@ -123,7 +124,7 @@ namespace Avalonia.Data } else { - converted = values.ToArray(); + converted = values; } if (converted == null) From 08d3b9154d83051e1decbee347a03cac5b508883 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 5 Nov 2020 14:40:59 +0300 Subject: [PATCH 32/41] Extract microcom generator stuff into a separate .targets file --- build/MicroCom.targets | 33 ++++++++++++++++++++++ src/Avalonia.Native/Avalonia.Native.csproj | 33 ++-------------------- 2 files changed, 35 insertions(+), 31 deletions(-) create mode 100644 build/MicroCom.targets diff --git a/build/MicroCom.targets b/build/MicroCom.targets new file mode 100644 index 0000000000..3a07950616 --- /dev/null +++ b/build/MicroCom.targets @@ -0,0 +1,33 @@ + + + + + + false + all + true + + + + + + + + + + + + + + + + + + <_AvaloniaPatchComInterop>true + + + diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index a08dc0f28f..3913484431 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -20,36 +20,7 @@ + - - - - - false - all - true - - - - - - - - - - - - - - - - - - <_AvaloniaPatchComInterop>true - - + From 47d6ba61dd7014087d6195e628420fa922d25694 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Thu, 5 Nov 2020 19:45:52 +0100 Subject: [PATCH 33/41] Update tests/Avalonia.Markup.UnitTests/Extensions/IEnummerableExtension.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes code smell Co-authored-by: Dariusz Komosiński --- .../Extensions/IEnummerableExtension.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.UnitTests/Extensions/IEnummerableExtension.cs b/tests/Avalonia.Markup.UnitTests/Extensions/IEnummerableExtension.cs index 33be06baed..0d9e2969e1 100644 --- a/tests/Avalonia.Markup.UnitTests/Extensions/IEnummerableExtension.cs +++ b/tests/Avalonia.Markup.UnitTests/Extensions/IEnummerableExtension.cs @@ -3,7 +3,7 @@ using System.Collections; namespace Avalonia.Markup.UnitTests { - static class IEnummerableExtension + internal static class IEnumerableExtensions { public static object ElementAt(this IEnumerable source, int index) { From e32e092e4ee5be67e5327d697e3f9b3b8e8a372c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 5 Nov 2020 22:04:02 +0000 Subject: [PATCH 34/41] WinUI Comp tweak backdrop blur. --- .../Avalonia.Win32/Composition/CompositionConnector.cs | 6 +----- src/Windows/Avalonia.Win32/Composition/SaturationEffect.cs | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Composition/CompositionConnector.cs b/src/Windows/Avalonia.Win32/Composition/CompositionConnector.cs index 5bdc0cd410..e0dfdf282c 100644 --- a/src/Windows/Avalonia.Win32/Composition/CompositionConnector.cs +++ b/src/Windows/Avalonia.Win32/Composition/CompositionConnector.cs @@ -127,17 +127,13 @@ namespace Avalonia.Win32 private SpriteVisual CreateBlur() { var blurEffect = new GaussianBlurEffect(new CompositionEffectSourceParameter("backdrop")); - var blurEffectFactory = _compositor.CreateEffectFactory(blurEffect); - var blurBrush = blurEffectFactory.CreateBrush(); var backDropBrush = _compositor.CreateBackdropBrush(); - blurBrush.SetSourceParameter("backdrop", backDropBrush); - var saturateEffect = new SaturationEffect(blurEffect); var satEffectFactory = _compositor.CreateEffectFactory(saturateEffect); - var satBrush = satEffectFactory.CreateBrush(); + satBrush.SetSourceParameter("backdrop", backDropBrush); var visual = _compositor.CreateSpriteVisual(); diff --git a/src/Windows/Avalonia.Win32/Composition/SaturationEffect.cs b/src/Windows/Avalonia.Win32/Composition/SaturationEffect.cs index 90eca22d8e..3495829f3a 100644 --- a/src/Windows/Avalonia.Win32/Composition/SaturationEffect.cs +++ b/src/Windows/Avalonia.Win32/Composition/SaturationEffect.cs @@ -6,7 +6,7 @@ namespace Avalonia.Win32 { class SaturationEffect : EffectBase { - public SaturationEffect(IGraphicsEffect source) : base(source) + public SaturationEffect(IGraphicsEffectSource source) : base(source) { } From 8946f4f80d17e848b16639c033470b9eff84f294 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 6 Nov 2020 17:51:10 +0000 Subject: [PATCH 35/41] prevent NRE when popup is open and main window closes. --- src/Avalonia.Native/WindowImplBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 2cf1fe2963..41009232e4 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -155,6 +155,7 @@ namespace Avalonia.Native } finally { + _parent._native = null; n?.Dispose(); } From eecc91feaa59e8d2e27b37440ca0fa3e9d5debb4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 7 Nov 2020 19:51:56 +0000 Subject: [PATCH 36/41] cleaner fix. --- src/Avalonia.Native/WindowImplBase.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 41009232e4..20b6b8ecc5 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -155,11 +155,10 @@ namespace Avalonia.Native } finally { - _parent._native = null; + + _parent?.Dispose(); n?.Dispose(); } - - _parent._mouse.Dispose(); } void IAvnWindowBaseEvents.Activated() => _parent.Activated?.Invoke(); @@ -337,6 +336,7 @@ namespace Avalonia.Native _nativeControlHost = null; (Screen as ScreenImpl)?.Dispose(); + _mouse.Dispose(); } From 177d18463a5fb35c318f67a8fe74f1efd1167c8f Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 7 Nov 2020 16:17:21 +0100 Subject: [PATCH 37/41] Add support for visualizing layout properties. --- .../ViewModels/ControlDetailsViewModel.cs | 6 + .../ViewModels/ControlLayoutViewModel.cs | 150 ++++++++++++++++++ .../Diagnostics/Views/ControlDetailsView.xaml | 145 +++++++++++++---- .../Diagnostics/Views/ThicknessEditor.cs | 117 ++++++++++++++ 4 files changed, 387 insertions(+), 31 deletions(-) create mode 100644 src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs create mode 100644 src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index d4b988acd4..fa41eacbeb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -32,6 +32,8 @@ namespace Avalonia.Diagnostics.ViewModels view.Filter = FilterProperty; PropertiesView = view; + Layout = new ControlLayoutViewModel(control); + if (control is INotifyPropertyChanged inpc) { inpc.PropertyChanged += ControlPropertyChanged; @@ -52,6 +54,8 @@ namespace Avalonia.Diagnostics.ViewModels get => _selectedProperty; set => RaiseAndSetIfChanged(ref _selectedProperty, value); } + + public ControlLayoutViewModel Layout { get; } public void Dispose() { @@ -112,6 +116,8 @@ namespace Avalonia.Diagnostics.ViewModels property.Update(); } } + + Layout.ControlPropertyChanged(sender, e); } private void ControlPropertyChanged(object sender, PropertyChangedEventArgs e) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs new file mode 100644 index 0000000000..0173f19358 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs @@ -0,0 +1,150 @@ +using System.ComponentModel; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.VisualTree; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class ControlLayoutViewModel : ViewModelBase + { + private readonly IVisual _control; + private Thickness _marginThickness; + private Thickness _borderThickness; + private Thickness _paddingThickness; + private string _sizeText; + + public Thickness MarginThickness + { + get => _marginThickness; + set => RaiseAndSetIfChanged(ref _marginThickness, value); + } + + public Thickness BorderThickness + { + get => _borderThickness; + set => RaiseAndSetIfChanged(ref _borderThickness, value); + } + + public Thickness PaddingThickness + { + get => _paddingThickness; + set => RaiseAndSetIfChanged(ref _paddingThickness, value); + } + + public string SizeText + { + get => _sizeText; + private set => RaiseAndSetIfChanged(ref _sizeText, value); + } + + public bool HasPadding { get; } + + public bool HasBorder { get; } + + public ControlLayoutViewModel(IVisual control) + { + _control = control; + + HasPadding = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Decorator.PaddingProperty); + HasBorder = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Border.BorderThicknessProperty); + + if (control is AvaloniaObject ao) + { + MarginThickness = ao.GetValue(Layoutable.MarginProperty); + + if (HasPadding) + { + PaddingThickness = ao.GetValue(Decorator.PaddingProperty); + } + + if (HasBorder) + { + BorderThickness = ao.GetValue(Border.BorderThicknessProperty); + } + } + + UpdateSize(); + } + + private bool _updatingFromControl; + + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (_updatingFromControl) + { + return; + } + + if (_control is AvaloniaObject ao) + { + if (e.PropertyName == nameof(MarginThickness)) + { + ao.SetValue(Layoutable.MarginProperty, MarginThickness); + } + else if (HasPadding && e.PropertyName == nameof(PaddingThickness)) + { + ao.SetValue(Decorator.PaddingProperty, PaddingThickness); + } + else if (HasBorder && e.PropertyName == nameof(BorderThickness)) + { + ao.SetValue(Border.BorderThicknessProperty, BorderThickness); + } + } + } + + public void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + try + { + _updatingFromControl = true; + + var updateSize = false; + + if (e.Property == Visual.BoundsProperty) + { + updateSize = true; + } + else + { + if (_control is IAvaloniaObject ao) + { + if (e.Property == Layoutable.MarginProperty) + { + MarginThickness = ao.GetValue(Layoutable.MarginProperty); + } + else if (e.Property == Decorator.PaddingProperty) + { + PaddingThickness = ao.GetValue(Decorator.PaddingProperty); + } + else if (e.Property == Border.BorderThicknessProperty) + { + BorderThickness = ao.GetValue(Border.BorderThicknessProperty); + + updateSize = true; + } + } + } + + if (updateSize) + { + UpdateSize(); + } + } + finally + { + _updatingFromControl = false; + } + } + + private void UpdateSize() + { + var size = _control.Bounds; + + size.Deflate(BorderThickness); + + SizeText = $"{size.Width} × {size.Height}"; + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 8aad5fffd8..1487cb6dde 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -1,38 +1,121 @@  - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs new file mode 100644 index 0000000000..58ea91c2fa --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs @@ -0,0 +1,117 @@ +using Avalonia.Controls; +using Avalonia.Data; + +namespace Avalonia.Diagnostics.Views +{ + internal class ThicknessEditor : ContentControl + { + public static readonly DirectProperty ThicknessProperty = + AvaloniaProperty.RegisterDirect(nameof(Thickness), o => o.Thickness, + (o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay); + + public static readonly DirectProperty HeaderProperty = + AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, + (o, v) => o.Header = v); + + public static readonly DirectProperty IsPresentProperty = + AvaloniaProperty.RegisterDirect(nameof(Header), o => o.IsPresent, + (o, v) => o.IsPresent = v); + + public static readonly DirectProperty LeftProperty = + AvaloniaProperty.RegisterDirect(nameof(Left), o => o.Left, (o, v) => o.Left = v); + + public static readonly DirectProperty TopProperty = + AvaloniaProperty.RegisterDirect(nameof(Top), o => o.Top, (o, v) => o.Top = v); + + public static readonly DirectProperty RightProperty = + AvaloniaProperty.RegisterDirect(nameof(Right), o => o.Right, + (o, v) => o.Right = v); + + public static readonly DirectProperty BottomProperty = + AvaloniaProperty.RegisterDirect(nameof(Bottom), o => o.Bottom, + (o, v) => o.Bottom = v); + + + private Thickness _thickness; + private string _header; + private bool _isPresent = true; + private double _left; + private double _top; + private double _right; + private double _bottom; + + private bool _isUpdatingThickness; + + public Thickness Thickness + { + get => _thickness; + set => SetAndRaise(ThicknessProperty, ref _thickness, value); + } + + public string Header + { + get => _header; + set => SetAndRaise(HeaderProperty, ref _header, value); + } + + public bool IsPresent + { + get => _isPresent; + set => SetAndRaise(IsPresentProperty, ref _isPresent, value); + } + + public double Left + { + get => _left; + set => SetAndRaise(LeftProperty, ref _left, value); + } + + public double Top + { + get => _top; + set => SetAndRaise(TopProperty, ref _top, value); + } + + public double Right + { + get => _right; + set => SetAndRaise(RightProperty, ref _right, value); + } + + public double Bottom + { + get => _bottom; + set => SetAndRaise(BottomProperty, ref _bottom, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ThicknessProperty) + { + try + { + _isUpdatingThickness = true; + + var value = change.NewValue.GetValueOrDefault(); + + Left = value.Left; + Top = value.Top; + Right = value.Right; + Bottom = value.Bottom; + } + finally + { + _isUpdatingThickness = false; + } + } + else if (!_isUpdatingThickness && + (change.Property == LeftProperty || change.Property == TopProperty || + change.Property == RightProperty || change.Property == BottomProperty)) + { + Thickness = new Thickness(Left, Top, Right, Bottom); + } + } + } +} From 91fec98a75e247054a93de7dc7d28c1ba6562415 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 7 Nov 2020 16:21:07 +0100 Subject: [PATCH 38/41] Move field. --- .../Diagnostics/ViewModels/ControlLayoutViewModel.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs index 0173f19358..2ee8df78ef 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs @@ -12,6 +12,7 @@ namespace Avalonia.Diagnostics.ViewModels private Thickness _borderThickness; private Thickness _paddingThickness; private string _sizeText; + private bool _updatingFromControl; public Thickness MarginThickness { @@ -66,8 +67,6 @@ namespace Avalonia.Diagnostics.ViewModels UpdateSize(); } - private bool _updatingFromControl; - protected override void OnPropertyChanged(PropertyChangedEventArgs e) { base.OnPropertyChanged(e); From 77ecdc0013b89d58ca13624610af165c45a1452e Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 7 Nov 2020 22:51:45 +0100 Subject: [PATCH 39/41] Display control size accurately and add info about sizing constraints. --- .../ViewModels/ControlLayoutViewModel.cs | 75 +++++++++--- .../Diagnostics/Views/ControlDetailsView.xaml | 47 ++++++-- .../Views/ControlDetailsView.xaml.cs | 109 ++++++++++++++++++ .../Diagnostics/Views/ThicknessEditor.cs | 13 +++ 4 files changed, 220 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs index 2ee8df78ef..fd2e4c3355 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs @@ -11,7 +11,10 @@ namespace Avalonia.Diagnostics.ViewModels private Thickness _marginThickness; private Thickness _borderThickness; private Thickness _paddingThickness; - private string _sizeText; + private double _width; + private double _height; + private string _widthConstraint; + private string _heightConstraint; private bool _updatingFromControl; public Thickness MarginThickness @@ -32,10 +35,28 @@ namespace Avalonia.Diagnostics.ViewModels set => RaiseAndSetIfChanged(ref _paddingThickness, value); } - public string SizeText + public double Width { - get => _sizeText; - private set => RaiseAndSetIfChanged(ref _sizeText, value); + get => _width; + private set => RaiseAndSetIfChanged(ref _width, value); + } + + public double Height + { + get => _height; + private set => RaiseAndSetIfChanged(ref _height, value); + } + + public string WidthConstraint + { + get => _widthConstraint; + private set => RaiseAndSetIfChanged(ref _widthConstraint, value); + } + + public string HeightConstraint + { + get => _heightConstraint; + private set => RaiseAndSetIfChanged(ref _heightConstraint, value); } public bool HasPadding { get; } @@ -65,6 +86,29 @@ namespace Avalonia.Diagnostics.ViewModels } UpdateSize(); + UpdateSizeConstraints(); + } + + private void UpdateSizeConstraints() + { + if (_control is IAvaloniaObject ao) + { + string CreateConstraintInfo(StyledProperty minProperty, StyledProperty maxProperty) + { + if (ao.IsSet(minProperty) || ao.IsSet(maxProperty)) + { + var minValue = ao.GetValue(minProperty); + var maxValue = ao.GetValue(maxProperty); + + return $"{minValue} < size < {maxValue}"; + } + + return null; + } + + WidthConstraint = CreateConstraintInfo(Layoutable.MinWidthProperty, Layoutable.MaxWidthProperty); + HeightConstraint = CreateConstraintInfo(Layoutable.MinHeightProperty, Layoutable.MaxHeightProperty); + } } protected override void OnPropertyChanged(PropertyChangedEventArgs e) @@ -99,11 +143,9 @@ namespace Avalonia.Diagnostics.ViewModels { _updatingFromControl = true; - var updateSize = false; - if (e.Property == Visual.BoundsProperty) { - updateSize = true; + UpdateSize(); } else { @@ -120,16 +162,16 @@ namespace Avalonia.Diagnostics.ViewModels else if (e.Property == Border.BorderThicknessProperty) { BorderThickness = ao.GetValue(Border.BorderThicknessProperty); - - updateSize = true; + } + else if (e.Property == Layoutable.MinWidthProperty || + e.Property == Layoutable.MaxWidthProperty || + e.Property == Layoutable.MinHeightProperty || + e.Property == Layoutable.MaxHeightProperty) + { + UpdateSizeConstraints(); } } } - - if (updateSize) - { - UpdateSize(); - } } finally { @@ -141,9 +183,8 @@ namespace Avalonia.Diagnostics.ViewModels { var size = _control.Bounds; - size.Deflate(BorderThickness); - - SizeText = $"{size.Width} × {size.Height}"; + Width = size.Width; + Height = size.Height; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 1487cb6dde..42e64d59ae 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -106,15 +106,48 @@ - - - - - - + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs index c6bd5a18aa..c9568509f6 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs @@ -1,10 +1,24 @@ +using System; using Avalonia.Controls; +using Avalonia.Controls.Shapes; using Avalonia.Markup.Xaml; +using Avalonia.VisualTree; namespace Avalonia.Diagnostics.Views { internal class ControlDetailsView : UserControl { + private ThicknessEditor _borderArea; + private ThicknessEditor _paddingArea; + private Rectangle _horizontalSizeBegin; + private Rectangle _horizontalSizeEnd; + private Rectangle _verticalSizeBegin; + private Rectangle _verticalSizeEnd; + private Grid _layoutRoot; + private Border _horizontalSize; + private Border _verticalSize; + private Border _contentArea; + public ControlDetailsView() { InitializeComponent(); @@ -13,6 +27,101 @@ namespace Avalonia.Diagnostics.Views private void InitializeComponent() { AvaloniaXamlLoader.Load(this); + + _borderArea = this.FindControl("BorderArea"); + _paddingArea = this.FindControl("PaddingArea"); + + _horizontalSizeBegin = this.FindControl("HorizontalSizeBegin"); + _horizontalSizeEnd = this.FindControl("HorizontalSizeEnd"); + _verticalSizeBegin = this.FindControl("VerticalSizeBegin"); + _verticalSizeEnd = this.FindControl("VerticalSizeEnd"); + + _horizontalSize = this.FindControl("HorizontalSize"); + _verticalSize = this.FindControl("VerticalSize"); + + _contentArea = this.FindControl("ContentArea"); + + _layoutRoot = this.FindControl("LayoutRoot"); + + void SubscribeToBounds(Visual visual) + { + visual.GetPropertyChangedObservable(TransformedBoundsProperty) + .Subscribe(UpdateSizeGuidelines); + } + + SubscribeToBounds(_borderArea); + SubscribeToBounds(_paddingArea); + SubscribeToBounds(_contentArea); + } + + private void UpdateSizeGuidelines(AvaloniaPropertyChangedEventArgs e) + { + void UpdateGuidelines(Visual area) + { + if (area.TransformedBounds is TransformedBounds bounds) + { + // Horizontal guideline + { + var sizeArea = TranslateToRoot((_horizontalSize.TransformedBounds ?? default).Bounds.BottomLeft, + _horizontalSize); + + var start = TranslateToRoot(bounds.Bounds.BottomLeft, area); + + SetPosition(_horizontalSizeBegin, start); + + var end = TranslateToRoot(bounds.Bounds.BottomRight, area); + + SetPosition(_horizontalSizeEnd, end.WithX(end.X - 1)); + + var height = sizeArea.Y - start.Y + 2; + + _horizontalSizeBegin.Height = height; + _horizontalSizeEnd.Height = height; + } + + // Vertical guideline + { + var sizeArea = TranslateToRoot((_verticalSize.TransformedBounds ?? default).Bounds.TopRight, _verticalSize); + + var start = TranslateToRoot(bounds.Bounds.TopRight, area); + + SetPosition(_verticalSizeBegin, start); + + var end = TranslateToRoot(bounds.Bounds.BottomRight, area); + + SetPosition(_verticalSizeEnd, end.WithY(end.Y - 1)); + + var width = sizeArea.X - start.X + 2; + + _verticalSizeBegin.Width = width; + _verticalSizeEnd.Width = width; + } + } + } + + Point TranslateToRoot(Point point, IVisual from) + { + return from.TranslatePoint(point, _layoutRoot) ?? default; + } + + static void SetPosition(Rectangle rect, Point start) + { + Canvas.SetLeft(rect, start.X); + Canvas.SetTop(rect, start.Y); + } + + if (_borderArea.IsPresent) + { + UpdateGuidelines(_borderArea); + } + else if (_paddingArea.IsPresent) + { + UpdateGuidelines(_paddingArea); + } + else + { + UpdateGuidelines(_contentArea); + } } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs index 58ea91c2fa..c7611c8c46 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ThicknessEditor.cs @@ -1,8 +1,21 @@ using Avalonia.Controls; using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Media; namespace Avalonia.Diagnostics.Views { + internal static class Converters + { + public static IValueConverter HasConstraintConverter = + new FuncValueConverter(ConvertToDecoration); + + private static TextDecorationCollection ConvertToDecoration(object arg) + { + return arg != null ? TextDecorations.Underline : null; + } + } + internal class ThicknessEditor : ContentControl { public static readonly DirectProperty ThicknessProperty = From 145d4a3ccb0d16887aebeeb2f7df69ca6ba43b7b Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 8 Nov 2020 17:18:25 +0100 Subject: [PATCH 40/41] Adjust sizing a bit. --- .../Diagnostics/Views/ControlDetailsView.xaml | 5 +++-- src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 42e64d59ae..2e0b6813ba 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -6,6 +6,7 @@ + @@ -62,7 +63,7 @@ - + @@ -137,7 +138,7 @@ diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml index 98de9b611e..86137dfc57 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels" x:Class="Avalonia.Diagnostics.Views.TreePageView"> - + Date: Sun, 8 Nov 2020 22:17:11 +0100 Subject: [PATCH 41/41] Add a single argument Parse function for FontFamily. --- src/Avalonia.Visuals/Media/FontFamily.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/FontFamily.cs b/src/Avalonia.Visuals/Media/FontFamily.cs index 9db49200cd..f018733235 100644 --- a/src/Avalonia.Visuals/Media/FontFamily.cs +++ b/src/Avalonia.Visuals/Media/FontFamily.cs @@ -133,6 +133,16 @@ namespace Avalonia.Media } } + /// + /// Parses a string. + /// + /// The string. + /// + /// + /// Specified family is not supported. + /// + public static FontFamily Parse(string s) => Parse(s, null); + /// /// Parses a string. /// @@ -142,7 +152,7 @@ namespace Avalonia.Media /// /// Specified family is not supported. /// - public static FontFamily Parse(string s, Uri baseUri = null) + public static FontFamily Parse(string s, Uri baseUri) { if (string.IsNullOrEmpty(s)) {