From ba6adb78c19f502d71a50e7ad818f8b343d34c04 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 13 May 2020 10:06:59 -0400 Subject: [PATCH 001/113] Add failing test for no items in TabControl --- .../TabControlTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 24aacd4000..db9211ac3c 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; @@ -325,6 +327,28 @@ namespace Avalonia.Controls.UnitTests Assert.NotEqual(dataContext, tabItem.Content); } + [Fact] + public void Can_Have_Empty_Tab_Control() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new Markup.Xaml.AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var tabControl = window.FindControl("tabs"); + + tabControl.DataContext = new { Tabs = new List() }; + window.ApplyTemplate(); + + Assert.Equal(0, tabControl.Items.Count()); + } + } + private IControlTemplate TabControlTemplate() { return new FuncControlTemplate((parent, scope) => From c1dc0017d4b794420a3b8049acb09833ea14ff55 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 13 May 2020 11:17:00 -0400 Subject: [PATCH 002/113] Add tests from chat with @grokys --- .../SelectionModelTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index 246ff723a1..60fe9f3674 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1752,6 +1752,26 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, node.PropertyChangedSubscriptions); } + [Fact] + public void Setting_SelectedIndex_To_Minus_1_Clears_Selection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { Source = data }; + target.SelectedIndex = new IndexPath(1); + target.SelectedIndex = new IndexPath(-1); + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Assigning_Source_With_Less_Items_Than_Selection_Trims_Selection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { RetainSelectionOnReset = true }; + target.SelectedIndex = new IndexPath(4); + target.Source = data; + Assert.Empty(target.SelectedIndices); + } + private int GetSubscriberCount(AvaloniaList list) { return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0; From 62e7dbded926a33415b5c2d636dced2203f4b4aa Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 13 May 2020 11:51:22 -0400 Subject: [PATCH 003/113] Add more tests for selection adjustments --- .../SelectionModelTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index 60fe9f3674..9d795f6247 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1762,6 +1762,18 @@ namespace Avalonia.Controls.UnitTests Assert.Empty(target.SelectedIndices); } + [Fact] + public void Assigning_Source_With_Less_Items_Than_Previous_Clears_Selection() + { + var data = new[] { "foo", "bar", "baz", "boo", "hoo" }; + var smallerData = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { RetainSelectionOnReset = true }; + target.Source = data; + target.SelectedIndex = new IndexPath(4); + target.Source = smallerData; + Assert.Empty(target.SelectedIndices); + } + [Fact] public void Assigning_Source_With_Less_Items_Than_Selection_Trims_Selection() { @@ -1772,6 +1784,18 @@ namespace Avalonia.Controls.UnitTests Assert.Empty(target.SelectedIndices); } + [Fact] + public void Assigning_Source_With_Less_Items_Than_Multiple_Selection_Trims_Selection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { RetainSelectionOnReset = true }; + target.Select(4); + target.Select(2); + target.Source = data; + Assert.Equal(1, target.SelectedIndices.Count); + Assert.Equal(new IndexPath(2), target.SelectedIndices.First()); + } + private int GetSubscriberCount(AvaloniaList list) { return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0; From 8fa6c05b7da7178d627eb8826d134a06f5c63083 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 13 May 2020 12:10:06 -0400 Subject: [PATCH 004/113] Another failing test for selection handling --- .../SelectionModelTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index 9d795f6247..211fc44caf 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1796,6 +1796,19 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new IndexPath(2), target.SelectedIndices.First()); } + // test that going from non-null source to non-null source clears selection + [Fact] + public void Changing_Source_With_Valid_IndexPath_Retains_Selection() + { + var data = new[] { "foo", "bar", "baz", "boo", "hoo" }; + var smallerData = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { RetainSelectionOnReset = true }; + target.Source = data; + target.SelectedIndex = new IndexPath(2); + target.Source = smallerData; + Assert.Equal(1, target.SelectedIndices.Count); + } + private int GetSubscriberCount(AvaloniaList list) { return ((INotifyCollectionChangedDebug)list).GetCollectionChangedSubscribers()?.Length ?? 0; From cd37520b7141c8d0140be887e8ee0fcc85471c6a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 25 May 2020 16:43:22 +0200 Subject: [PATCH 005/113] Trim invalid selection when Source first assigned. `SelectionModel` can have a selection set before its `Source` is initialized. In this case, be sure to trim invalid selections from the model before continuing. Fixes #3919 --- src/Avalonia.Controls/IndexRange.cs | 47 +++++++++++ src/Avalonia.Controls/SelectionModel.cs | 7 +- .../SelectionModelChangeSet.cs | 2 +- src/Avalonia.Controls/SelectionNode.cs | 21 +++++ .../IndexRangeTests.cs | 82 +++++++++++++++++++ .../SelectionModelTests.cs | 44 +++++++--- 6 files changed, 187 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/IndexRange.cs b/src/Avalonia.Controls/IndexRange.cs index 1dc161c699..e45d013af4 100644 --- a/src/Avalonia.Controls/IndexRange.cs +++ b/src/Avalonia.Controls/IndexRange.cs @@ -132,6 +132,53 @@ namespace Avalonia.Controls return result; } + public static int Intersect( + IList ranges, + IndexRange range, + IList? removed = null) + { + var result = 0; + + for (var i = 0; i < ranges.Count && range != s_invalid; ++i) + { + var existing = ranges[i]; + + if (existing.End < range.Begin || existing.Begin > range.End) + { + removed?.Add(existing); + ranges.RemoveAt(i--); + result += existing.Count; + } + else + { + if (existing.Begin < range.Begin) + { + var except = new IndexRange(existing.Begin, range.Begin - 1); + removed?.Add(except); + ranges[i] = existing = new IndexRange(range.Begin, existing.End); + result += except.Count; + } + + if (existing.End > range.End) + { + var except = new IndexRange(range.End + 1, existing.End); + removed?.Add(except); + ranges[i] = new IndexRange(existing.Begin, range.End); + result += except.Count; + } + } + } + + MergeRanges(ranges); + + if (removed is object) + { + MergeRanges(removed); + } + + return result; + } + public static int Remove( IList ranges, IndexRange range, diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index dd4934f9e5..7a64421326 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -45,12 +45,9 @@ namespace Avalonia.Controls if (_rootNode.Source != null) { - if (_rootNode.Source != null) + using (var operation = new Operation(this)) { - using (var operation = new Operation(this)) - { - ClearSelection(resetAnchor: true); - } + ClearSelection(resetAnchor: true); } } diff --git a/src/Avalonia.Controls/SelectionModelChangeSet.cs b/src/Avalonia.Controls/SelectionModelChangeSet.cs index 6e77dc5755..d1df38656a 100644 --- a/src/Avalonia.Controls/SelectionModelChangeSet.cs +++ b/src/Avalonia.Controls/SelectionModelChangeSet.cs @@ -135,7 +135,7 @@ namespace Avalonia.Controls if (index >= currentIndex && index < currentIndex + currentCount) { int targetIndex = GetIndexAt(getRanges(info), index - currentIndex); - item = info.Items?.GetAt(targetIndex); + item = info.Items?.Count > targetIndex ? info.Items?.GetAt(targetIndex) : null; break; } diff --git a/src/Avalonia.Controls/SelectionNode.cs b/src/Avalonia.Controls/SelectionNode.cs index 0b00db88c3..1a3bde1765 100644 --- a/src/Avalonia.Controls/SelectionNode.cs +++ b/src/Avalonia.Controls/SelectionNode.cs @@ -101,6 +101,7 @@ namespace Avalonia.Controls ItemsSourceView = newDataSource; + TrimInvalidSelections(); PopulateSelectedItemsFromSelectedIndices(); HookupCollectionChangedHandler(); OnSelectionChanged(); @@ -108,6 +109,26 @@ namespace Avalonia.Controls } } + private void TrimInvalidSelections() + { + if (_selected == null || ItemsSourceView == null) + { + return; + } + + var validRange = ItemsSourceView.Count > 0 ? new IndexRange(0, ItemsSourceView.Count - 1) : new IndexRange(-1, -1); + var removed = new List(); + var removedCount = IndexRange.Intersect(_selected, validRange, removed); + + if (removedCount > 0) + { + using var operation = _manager.Update(); + SelectedCount -= removedCount; + OnSelectionChanged(); + _operation!.Deselected(removed); + } + } + public ItemsSourceView? ItemsSourceView { get; private set; } public int DataCount => ItemsSourceView?.Count ?? 0; public int ChildrenNodeCount => _childrenNodes.Count; diff --git a/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs b/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs index e0f46d9fa9..e01c752658 100644 --- a/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/IndexRangeTests.cs @@ -127,6 +127,88 @@ namespace Avalonia.Controls.UnitTests Assert.Empty(selected); } + [Fact] + public void Intersect_Should_Remove_Items_From_Beginning() + { + var ranges = new List { new IndexRange(0, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(2, 12), removed); + + Assert.Equal(2, result); + Assert.Equal(new[] { new IndexRange(2, 10) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 1) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Items_From_End() + { + var ranges = new List { new IndexRange(0, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(0, 8), removed); + + Assert.Equal(2, result); + Assert.Equal(new[] { new IndexRange(0, 8) }, ranges); + Assert.Equal(new[] { new IndexRange(9, 10) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_Range_Start() + { + var ranges = new List { new IndexRange(0, 5), new IndexRange(6, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(6, 10), removed); + + Assert.Equal(6, result); + Assert.Equal(new[] { new IndexRange(6, 10) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 5) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_Range_End() + { + var ranges = new List { new IndexRange(0, 5), new IndexRange(6, 10) }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(0, 4), removed); + + Assert.Equal(6, result); + Assert.Equal(new[] { new IndexRange(0, 4) }, ranges); + Assert.Equal(new[] { new IndexRange(5, 10) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_Range_Start_End() + { + var ranges = new List + { + new IndexRange(0, 2), + new IndexRange(3, 7), + new IndexRange(8, 10) + }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(3, 7), removed); + + Assert.Equal(6, result); + Assert.Equal(new[] { new IndexRange(3, 7) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 2), new IndexRange(8, 10) }, removed); + } + + [Fact] + public void Intersect_Should_Remove_Entire_And_Partial_Range_Start_End() + { + var ranges = new List + { + new IndexRange(0, 2), + new IndexRange(3, 7), + new IndexRange(8, 10) + }; + var removed = new List(); + var result = IndexRange.Intersect(ranges, new IndexRange(4, 6), removed); + + Assert.Equal(8, result); + Assert.Equal(new[] { new IndexRange(4, 6) }, ranges); + Assert.Equal(new[] { new IndexRange(0, 3), new IndexRange(7, 10) }, removed); + } + [Fact] public void Remove_Should_Remove_Entire_Range() { diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index 211fc44caf..1dc5fead26 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1775,7 +1775,17 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Assigning_Source_With_Less_Items_Than_Selection_Trims_Selection() + public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel(); + target.SelectedIndex = new IndexPath(4); + target.Source = data; + Assert.Empty(target.SelectedIndices); + } + + [Fact] + public void Initializing_Source_With_Less_Items_Than_Selection_Trims_Selection_RetainSelection() { var data = new[] { "foo", "bar", "baz" }; var target = new SelectionModel { RetainSelectionOnReset = true }; @@ -1785,7 +1795,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Assigning_Source_With_Less_Items_Than_Multiple_Selection_Trims_Selection() + public void Initializing_Source_With_Less_Items_Than_Multiple_Selection_Trims_Selection() { var data = new[] { "foo", "bar", "baz" }; var target = new SelectionModel { RetainSelectionOnReset = true }; @@ -1796,17 +1806,31 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new IndexPath(2), target.SelectedIndices.First()); } - // test that going from non-null source to non-null source clears selection [Fact] - public void Changing_Source_With_Valid_IndexPath_Retains_Selection() + public void Initializing_Source_With_Less_Items_Than_Selection_Raises_SelectionChanged() { - var data = new[] { "foo", "bar", "baz", "boo", "hoo" }; - var smallerData = new[] { "foo", "bar", "baz" }; - var target = new SelectionModel { RetainSelectionOnReset = true }; + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel(); + var raised = 0; + + target.SelectedIndex = new IndexPath(4); + + target.SelectionChanged += (s, e) => + { + if (raised == 0) + { + Assert.Equal(new[] { Path(4) }, e.DeselectedIndices); + Assert.Equal(new object[] { null }, e.DeselectedItems); + Assert.Empty(e.SelectedIndices); + Assert.Empty(e.SelectedItems); + } + + ++raised; + }; + target.Source = data; - target.SelectedIndex = new IndexPath(2); - target.Source = smallerData; - Assert.Equal(1, target.SelectedIndices.Count); + + Assert.Equal(2, raised); } private int GetSubscriberCount(AvaloniaList list) From 9dc9f9debae7cd082acaaf95276f2b49691754bc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 May 2020 13:17:02 +0200 Subject: [PATCH 006/113] Ensure selection is reported correctly during batch update. --- src/Avalonia.Controls/SelectionModel.cs | 13 ++++++ .../SelectionModelTests.cs | 41 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index 0a74306e84..a40e0a62f4 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -629,6 +629,8 @@ namespace Avalonia.Controls { AnchorIndex = default; } + + OnSelectionChanged(); } private void OnSelectionChanged(SelectionModelSelectionChangedEventArgs? e = null) @@ -664,6 +666,8 @@ namespace Avalonia.Controls { AnchorIndex = new IndexPath(index); } + + OnSelectionChanged(); } private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select) @@ -680,6 +684,8 @@ namespace Avalonia.Controls { AnchorIndex = new IndexPath(groupIndex, itemIndex); } + + OnSelectionChanged(); } private void SelectWithPathImpl(IndexPath index, bool select) @@ -708,6 +714,8 @@ namespace Avalonia.Controls { AnchorIndex = index; } + + OnSelectionChanged(); } private void SelectRangeFromAnchorImpl(int index, bool select) @@ -721,6 +729,7 @@ namespace Avalonia.Controls } _rootNode.SelectRange(new IndexRange(anchorIndex, index), select); + OnSelectionChanged(); } private void SelectRangeFromAnchorWithGroupImpl(int endGroupIndex, int endItemIndex, bool select) @@ -754,6 +763,8 @@ namespace Avalonia.Controls int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1; groupNode.SelectRange(new IndexRange(startIndex, endIndex), select); } + + OnSelectionChanged(); } private void SelectRangeImpl(IndexPath start, IndexPath end, bool select) @@ -781,6 +792,8 @@ namespace Avalonia.Controls info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select); } }); + + OnSelectionChanged(); } private void BeginOperation() diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index b9f93090b4..59552e7f93 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1512,6 +1512,47 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, raised); } + [Fact] + public void Batch_Update_Selection_Is_Correct_Throughout() + { + var data = new[] { "foo", "bar", "baz", "qux" }; + var target = new SelectionModel { Source = data }; + var raised = 0; + + using (target.Update()) + { + target.Select(1); + + Assert.Equal(new IndexPath(1), target.SelectedIndex); + Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + + target.Deselect(1); + + Assert.Equal(new IndexPath(), target.SelectedIndex); + Assert.Empty(target.SelectedIndices); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + + target.SelectRange(new IndexPath(1), new IndexPath(1)); + + Assert.Equal(new IndexPath(1), target.SelectedIndex); + Assert.Equal(new[] { new IndexPath(1) }, target.SelectedIndices); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + + target.ClearSelection(); + + Assert.Equal(new IndexPath(), target.SelectedIndex); + Assert.Empty(target.SelectedIndices); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + } + + Assert.Equal(0, raised); + } + [Fact] public void AutoSelect_Selects_When_Enabled() { From 2255518851914fa1c6558d833aad837f88d531ea Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 May 2020 13:28:28 +0200 Subject: [PATCH 007/113] Apply auto-select at end of batch update. --- src/Avalonia.Controls/SelectionModel.cs | 41 +++++++++++++------ .../SelectionModelTests.cs | 24 +++++++++++ 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index a40e0a62f4..314c36d28d 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -46,14 +46,25 @@ namespace Avalonia.Controls if (_rootNode.Source != null) { - using (var operation = new Operation(this)) + // Temporarily prevent auto-select when switching source. + var restoreAutoSelect = _autoSelect; + _autoSelect = false; + + try + { + using (var operation = new Operation(this)) + { + ClearSelection(resetAnchor: true); + } + } + finally { - ClearSelection(resetAnchor: true); + _autoSelect = restoreAutoSelect; } } _rootNode.Source = value; - ApplyAutoSelect(); + ApplyAutoSelect(true); RaisePropertyChanged("Source"); @@ -111,7 +122,7 @@ namespace Avalonia.Controls if (_autoSelect != value) { _autoSelect = value; - ApplyAutoSelect(); + ApplyAutoSelect(true); } } } @@ -185,7 +196,6 @@ namespace Avalonia.Controls using var operation = new Operation(this); ClearSelection(resetAnchor: true); SelectWithPathImpl(value, select: true); - ApplyAutoSelect(); } } } @@ -381,21 +391,18 @@ namespace Avalonia.Controls { using var operation = new Operation(this); SelectImpl(index, select: false); - ApplyAutoSelect(); } public void Deselect(int groupIndex, int itemIndex) { using var operation = new Operation(this); SelectWithGroupImpl(groupIndex, itemIndex, select: false); - ApplyAutoSelect(); } public void DeselectAt(IndexPath index) { using var operation = new Operation(this); SelectWithPathImpl(index, select: false); - ApplyAutoSelect(); } public bool IsSelected(int index) => _rootNode.IsSelected(index); @@ -562,7 +569,6 @@ namespace Avalonia.Controls { using var operation = new Operation(this); ClearSelection(resetAnchor: true); - ApplyAutoSelect(); } public IDisposable Update() => new Operation(this); @@ -589,7 +595,7 @@ namespace Avalonia.Controls } OnSelectionChanged(e); - ApplyAutoSelect(); + ApplyAutoSelect(true); } internal IObservable? ResolvePath(object data, IndexPath dataIndexPath) @@ -816,6 +822,8 @@ namespace Avalonia.Controls if (--_operationCount == 0) { + ApplyAutoSelect(false); + var changes = new List(); _rootNode.EndOperation(changes); @@ -837,7 +845,7 @@ namespace Avalonia.Controls } } - private void ApplyAutoSelect() + private void ApplyAutoSelect(bool createOperation) { if (AutoSelect) { @@ -845,8 +853,15 @@ namespace Avalonia.Controls if (SelectedIndex == default && _rootNode.ItemsSourceView?.Count > 0) { - using var operation = new Operation(this); - SelectImpl(0, true); + if (createOperation) + { + using var operation = new Operation(this); + SelectImpl(0, true); + } + else + { + SelectImpl(0, true); + } } } } diff --git a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs index 59552e7f93..24e82a69d0 100644 --- a/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SelectionModelTests.cs @@ -1754,6 +1754,30 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, raised); } + [Fact] + public void AutoSelect_Is_Applied_At_End_Of_Batch_Update() + { + var data = new[] { "foo", "bar", "baz" }; + var target = new SelectionModel { AutoSelect = true, Source = data }; + + using (target.Update()) + { + target.ClearSelection(); + + Assert.Equal(new IndexPath(), target.SelectedIndex); + Assert.Empty(target.SelectedIndices); + Assert.Null(target.SelectedItem); + Assert.Empty(target.SelectedItems); + } + + Assert.Equal(new IndexPath(0), target.SelectedIndex); + Assert.Equal(new[] { new IndexPath(0) }, target.SelectedIndices); + Assert.Equal("foo", target.SelectedItem); + Assert.Equal(new[] { "foo" }, target.SelectedItems); + + Assert.Equal(new IndexPath(0), target.SelectedIndex); + } + [Fact] public void Can_Replace_Parent_Children_Collection() { From d504c861993bb002635e64fc470ba80509769a05 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 13 May 2020 13:00:51 +0200 Subject: [PATCH 008/113] Added a "Select Random Item" button to TreeView page. Causes an exception due to containers not being materialized. --- samples/ControlCatalog/Pages/TreeViewPage.xaml | 1 + .../ViewModels/TreeViewPageViewModel.cs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index 6019d5f91f..789b45e62c 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -20,6 +20,7 @@ + Single diff --git a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs index d396ef2b3d..5bc23e2fe5 100644 --- a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs @@ -23,12 +23,14 @@ namespace ControlCatalog.ViewModels AddItemCommand = ReactiveCommand.Create(AddItem); RemoveItemCommand = ReactiveCommand.Create(RemoveItem); + SelectRandomItemCommand = ReactiveCommand.Create(SelectRandomItem); } public ObservableCollection Items { get; } public SelectionModel Selection { get; } public ReactiveCommand AddItemCommand { get; } public ReactiveCommand RemoveItemCommand { get; } + public ReactiveCommand SelectRandomItemCommand { get; } public SelectionMode SelectionMode { @@ -74,6 +76,15 @@ namespace ControlCatalog.ViewModels } } + private void SelectRandomItem() + { + var random = new Random(); + var depth = random.Next(4); + var indexes = Enumerable.Range(0, 4).Select(x => random.Next(10)); + var path = new IndexPath(indexes); + Selection.SelectedIndex = path; + } + private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) { var selected = string.Join(",", e.SelectedIndices); From 87868cd2bda806fd584a08860bb2222a05c0e7af Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 13 May 2020 14:30:22 +0200 Subject: [PATCH 009/113] Materialize TreeViewItems on selection. If we're selecting a particular tree view item, then materialize and expand the item's ancestors as `SelectionModel` requests children. Only do this if a particular item is being selected, not when an item is selected as part of a range select. To do this, needed to add a `FinalIndex` property to `SelectionModelChildrenRequestedEventArgs` in order to know if we're selecting a descendent of the item whose children are being requested. This is a massive hack, but I can't think of a better way to do it with the current `TreeView` implementation. --- src/Avalonia.Controls/IndexPath.cs | 20 ++++++++++++++ src/Avalonia.Controls/SelectionModel.cs | 27 ++++++++++++------- ...electionModelChildrenRequestedEventArgs.cs | 22 ++++++++++++++- src/Avalonia.Controls/SelectionNode.cs | 6 ++--- src/Avalonia.Controls/TreeView.cs | 9 ++++++- .../Utils/SelectionTreeHelper.cs | 8 +++--- 6 files changed, 73 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls/IndexPath.cs b/src/Avalonia.Controls/IndexPath.cs index 6c5aaf7ad1..73b75bc23d 100644 --- a/src/Avalonia.Controls/IndexPath.cs +++ b/src/Avalonia.Controls/IndexPath.cs @@ -123,6 +123,26 @@ namespace Avalonia.Controls } } + public bool IsAncestorOf(in IndexPath other) + { + if (other.GetSize() <= GetSize()) + { + return false; + } + + var size = GetSize(); + + for (int i = 0; i < size; i++) + { + if (GetAt(i) != other.GetAt(i)) + { + return false; + } + } + + return true; + } + public override string ToString() { if (_path != null) diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index 314c36d28d..ff1c0260bb 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -141,7 +141,7 @@ namespace Avalonia.Controls while (current?.AnchorIndex >= 0) { path.Add(current.AnchorIndex); - current = current.GetAt(current.AnchorIndex, false); + current = current.GetAt(current.AnchorIndex, false, default); } anchor = new IndexPath(path); @@ -420,7 +420,7 @@ namespace Avalonia.Controls for (int i = 0; i < path.GetSize() - 1; i++) { var childIndex = path.GetAt(i); - node = node.GetAt(childIndex, realizeChild: false); + node = node.GetAt(childIndex, false, default); if (node == null) { @@ -455,7 +455,7 @@ namespace Avalonia.Controls } var isSelected = (bool?)false; - var childNode = _rootNode.GetAt(groupIndex, realizeChild: false); + var childNode = _rootNode.GetAt(groupIndex, false, default); if (childNode != null) { @@ -474,7 +474,7 @@ namespace Avalonia.Controls for (int i = 0; i < path.GetSize() - 1; i++) { var childIndex = path.GetAt(i); - node = node.GetAt(childIndex, realizeChild: false); + node = node.GetAt(childIndex, false, default); if (node == null) { @@ -598,7 +598,10 @@ namespace Avalonia.Controls ApplyAutoSelect(true); } - internal IObservable? ResolvePath(object data, IndexPath dataIndexPath) + internal IObservable? ResolvePath( + object data, + IndexPath dataIndexPath, + IndexPath finalIndexPath) { IObservable? resolved = null; @@ -607,18 +610,22 @@ namespace Avalonia.Controls { if (_childrenRequestedEventArgs == null) { - _childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(data, dataIndexPath, false); + _childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs( + data, + dataIndexPath, + finalIndexPath, + false); } else { - _childrenRequestedEventArgs.Initialize(data, dataIndexPath, false); + _childrenRequestedEventArgs.Initialize(data, dataIndexPath, finalIndexPath, false); } ChildrenRequested(this, _childrenRequestedEventArgs); resolved = _childrenRequestedEventArgs.Children; // Clear out the values in the args so that it cannot be used after the event handler call. - _childrenRequestedEventArgs.Initialize(null, default, true); + _childrenRequestedEventArgs.Initialize(null, default, default, true); } return resolved; @@ -683,7 +690,7 @@ namespace Avalonia.Controls ClearSelection(resetAnchor: true); } - var childNode = _rootNode.GetAt(groupIndex, realizeChild: true); + var childNode = _rootNode.GetAt(groupIndex, true, new IndexPath(groupIndex, itemIndex)); var selected = childNode!.Select(itemIndex, select); if (selected) @@ -764,7 +771,7 @@ namespace Avalonia.Controls for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++) { - var groupNode = _rootNode.GetAt(groupIdx, realizeChild: true)!; + var groupNode = _rootNode.GetAt(groupIdx, true, new IndexPath(endGroupIndex, endItemIndex))!; int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0; int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1; groupNode.SelectRange(new IndexRange(startIndex, endIndex), select); diff --git a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs b/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs index 974da0cf71..b1f3e0b2c4 100644 --- a/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs +++ b/src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs @@ -16,15 +16,17 @@ namespace Avalonia.Controls { private object? _source; private IndexPath _sourceIndexPath; + private IndexPath _finalIndexPath; private bool _throwOnAccess; internal SelectionModelChildrenRequestedEventArgs( object source, IndexPath sourceIndexPath, + IndexPath finalIndexPath, bool throwOnAccess) { source = source ?? throw new ArgumentNullException(nameof(source)); - Initialize(source, sourceIndexPath, throwOnAccess); + Initialize(source, sourceIndexPath, finalIndexPath, throwOnAccess); } /// @@ -65,9 +67,26 @@ namespace Avalonia.Controls } } + /// + /// Gets the index of the final object which is being attempted to be retrieved. + /// + public IndexPath FinalIndex + { + get + { + if (_throwOnAccess) + { + throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs)); + } + + return _finalIndexPath; + } + } + internal void Initialize( object? source, IndexPath sourceIndexPath, + IndexPath finalIndexPath, bool throwOnAccess) { if (!throwOnAccess && source == null) @@ -77,6 +96,7 @@ namespace Avalonia.Controls _source = source; _sourceIndexPath = sourceIndexPath; + _finalIndexPath = finalIndexPath; _throwOnAccess = throwOnAccess; } } diff --git a/src/Avalonia.Controls/SelectionNode.cs b/src/Avalonia.Controls/SelectionNode.cs index 1a3bde1765..d99606673e 100644 --- a/src/Avalonia.Controls/SelectionNode.cs +++ b/src/Avalonia.Controls/SelectionNode.cs @@ -162,7 +162,7 @@ namespace Avalonia.Controls // create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid // an explosion of node objects. However, I'm still creating the m_childrenNodes // collection unfortunately. - public SelectionNode? GetAt(int index, bool realizeChild) + public SelectionNode? GetAt(int index, bool realizeChild, IndexPath finalIndexPath) { SelectionNode? child = null; @@ -192,7 +192,7 @@ namespace Avalonia.Controls if (childData != null) { var childDataIndexPath = IndexPath.CloneWithChildIndex(index); - resolver = _manager.ResolvePath(childData, childDataIndexPath); + resolver = _manager.ResolvePath(childData, childDataIndexPath, finalIndexPath); } if (resolver != null) @@ -864,7 +864,7 @@ namespace Avalonia.Controls int notSelectedCount = 0; for (int i = 0; i < ChildrenNodeCount; i++) { - var child = GetAt(i, realizeChild: false); + var child = GetAt(i, false, default); if (child != null) { diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 95e7437838..3e6ade39fc 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -395,10 +395,17 @@ namespace Avalonia.Controls private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e) { - var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as ItemsControl; + var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as TreeViewItem; if (container is object) { + if (e.SourceIndex.IsAncestorOf(e.FinalIndex)) + { + container.IsExpanded = true; + container.ApplyTemplate(); + container.Presenter?.ApplyTemplate(); + } + e.Children = Observable.CombineLatest( container.GetObservable(TreeViewItem.IsExpandedProperty), container.GetObservable(ItemsProperty), diff --git a/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs b/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs index 430ecabbb8..5adf5bdeea 100644 --- a/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs +++ b/src/Avalonia.Controls/Utils/SelectionTreeHelper.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls.Utils if (depth < path.GetSize() - 1) { - node = node.GetAt(childIndex, realizeChildren)!; + node = node.GetAt(childIndex, realizeChildren, path)!; } } } @@ -50,7 +50,7 @@ namespace Avalonia.Controls.Utils int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount; for (int i = count - 1; i >= 0; i--) { - var child = nextNode.Node.GetAt(i, realizeChildren); + var child = nextNode.Node.GetAt(i, realizeChildren, nextNode.Path); var childPath = nextNode.Path.CloneWithChildIndex(i); if (child != null) { @@ -90,7 +90,7 @@ namespace Avalonia.Controls.Utils for (int i = endIndex; i >= startIndex; i--) { - var child = node.GetAt(i, realizeChild: true); + var child = node.GetAt(i, true, end); if (child != null) { var childPath = currentPath.CloneWithChildIndex(i); @@ -112,7 +112,7 @@ namespace Avalonia.Controls.Utils int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : info.Node.DataCount - 1; for (int i = endIndex; i >= startIndex; i--) { - var child = info.Node.GetAt(i, realizeChild: true); + var child = info.Node.GetAt(i, true, end); if (child != null) { var childPath = info.Path.CloneWithChildIndex(i); From 9df84abbf3db84a74bc9fbee1b90e14b816a23d2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 13 May 2020 14:55:33 +0200 Subject: [PATCH 010/113] Bring selected TreeViewItem into view after layout finishes. --- src/Avalonia.Controls/TreeView.cs | 2 +- .../TreeViewTests.cs | 548 ++++++++++-------- 2 files changed, 298 insertions(+), 252 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 3e6ade39fc..1e66bf4d69 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -346,7 +346,7 @@ namespace Avalonia.Controls if (container != null) { - container.BringIntoView(); + DispatcherTimer.RunOnce(container.BringIntoView, TimeSpan.Zero); } } } diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 373f3e6861..c1bd45bcad 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -118,313 +118,340 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Clicking_Item_Should_Select_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); - Assert.NotNull(container); + Assert.NotNull(container); - _mouse.Click(container); + _mouse.Click(container); - Assert.Equal(item, target.SelectedItem); - Assert.True(container.IsSelected); + Assert.Equal(item, target.SelectedItem); + Assert.True(container.IsSelected); + } } [Fact] public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); - Assert.NotNull(container); + Assert.NotNull(container); - target.SelectedItem = item; + target.SelectedItem = item; - Assert.True(container.IsSelected); + Assert.True(container.IsSelected); - _mouse.Click(container, modifiers: KeyModifiers.Control); + _mouse.Click(container, modifiers: KeyModifiers.Control); - Assert.Null(target.SelectedItem); - Assert.False(container.IsSelected); + Assert.Null(target.SelectedItem); + Assert.False(container.IsSelected); + } } [Fact] public void Clicking_WithControlModifier_Not_Selected_Item_Should_Select_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item1 = tree[0].Children[1].Children[0]; - var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); + var item1 = tree[0].Children[1].Children[0]; + var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); - var item2 = tree[0].Children[1]; - var container2 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); + var item2 = tree[0].Children[1]; + var container2 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); - Assert.NotNull(container1); - Assert.NotNull(container2); + Assert.NotNull(container1); + Assert.NotNull(container2); - target.SelectedItem = item1; + target.SelectedItem = item1; - Assert.True(container1.IsSelected); + Assert.True(container1.IsSelected); - _mouse.Click(container2, modifiers: KeyModifiers.Control); - - Assert.Equal(item2, target.SelectedItem); - Assert.False(container1.IsSelected); - Assert.True(container2.IsSelected); + _mouse.Click(container2, modifiers: KeyModifiers.Control); + + Assert.Equal(item2, target.SelectedItem); + Assert.False(container1.IsSelected); + Assert.True(container2.IsSelected); + } } [Fact] public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_And_Remove_From_SelectedItems() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var item1 = rootNode.Children[0]; - var item2 = rootNode.Children.Last(); + var item1 = rootNode.Children[0]; + var item2 = rootNode.Children.Last(); - var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); - var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); + var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); + var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); - ClickContainer(item1Container, KeyModifiers.Control); - Assert.True(item1Container.IsSelected); + ClickContainer(item1Container, KeyModifiers.Control); + Assert.True(item1Container.IsSelected); - ClickContainer(item2Container, KeyModifiers.Control); - Assert.True(item2Container.IsSelected); + ClickContainer(item2Container, KeyModifiers.Control); + Assert.True(item2Container.IsSelected); - Assert.Equal(new[] {item1, item2}, target.Selection.SelectedItems.OfType()); + Assert.Equal(new[] { item1, item2 }, target.Selection.SelectedItems.OfType()); - ClickContainer(item1Container, KeyModifiers.Control); - Assert.False(item1Container.IsSelected); + ClickContainer(item1Container, KeyModifiers.Control); + Assert.False(item1Container.IsSelected); - Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType()); + Assert.DoesNotContain(item1, target.Selection.SelectedItems.OfType()); + } } [Fact] public void Clicking_WithShiftModifier_DownDirection_Should_Select_Range_Of_Items() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children.Last(); + var from = rootNode.Children[0]; + var to = rootNode.Children.Last(); - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); + } } [Fact] public void Clicking_WithShiftModifier_UpDirection_Should_Select_Range_Of_Items() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; + var from = rootNode.Children.Last(); + var to = rootNode.Children[0]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); + } } [Fact] public void Clicking_First_Item_Of_SelectedItems_Should_Select_Only_It() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var rootNode = tree[0]; + var rootNode = tree[0]; - var from = rootNode.Children.Last(); - var to = rootNode.Children[0]; + var from = rootNode.Children.Last(); + var to = rootNode.Children[0]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - ClickContainer(toContainer, KeyModifiers.Shift); - AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, KeyModifiers.Shift); + AssertChildrenSelected(target, rootNode); - ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(fromContainer, KeyModifiers.None); - Assert.True(fromContainer.IsSelected); + Assert.True(fromContainer.IsSelected); - foreach (var child in rootNode.Children) - { - if (child == from) + foreach (var child in rootNode.Children) { - continue; - } + if (child == from) + { + continue; + } - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(child); + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(child); - Assert.False(container.IsSelected); + Assert.False(container.IsSelected); + } } } [Fact] public void Setting_SelectedItem_Should_Set_Container_Selected() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); - Assert.NotNull(container); + Assert.NotNull(container); - target.SelectedItem = item; + target.SelectedItem = item; - Assert.True(container.IsSelected); + Assert.True(container.IsSelected); + } } [Fact] public void Setting_SelectedItem_Should_Raise_SelectedItemChanged_Event() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - ExpandAll(target); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); - var item = tree[0].Children[1].Children[0]; + var item = tree[0].Children[1].Children[0]; - var called = false; - target.SelectionChanged += (s, e) => - { - Assert.Empty(e.RemovedItems); - Assert.Equal(1, e.AddedItems.Count); - Assert.Same(item, e.AddedItems[0]); - called = true; - }; + var called = false; + target.SelectionChanged += (s, e) => + { + Assert.Empty(e.RemovedItems); + Assert.Equal(1, e.AddedItems.Count); + Assert.Same(item, e.AddedItems[0]); + called = true; + }; - target.SelectedItem = item; - Assert.True(called); + target.SelectedItem = item; + Assert.True(called); + } } [Fact] @@ -564,7 +591,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Keyboard_Navigation_Should_Move_To_Last_Selected_Node() { - using (UnitTestApplication.Start(TestServices.RealFocus)) + using (Application()) { var focus = FocusManager.Instance; var navigation = AvaloniaLocator.Current.GetService(); @@ -647,7 +674,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Pressing_SelectAll_Gesture_With_Downward_Range_Selected_Should_Select_All_Nodes() { - using (UnitTestApplication.Start()) + using (Application()) { var tree = CreateTestTreeData(); var target = new TreeView @@ -694,7 +721,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Pressing_SelectAll_Gesture_With_Upward_Range_Selected_Should_Select_All_Nodes() { - using (UnitTestApplication.Start()) + using (Application()) { var tree = CreateTestTreeData(); var target = new TreeView @@ -768,97 +795,106 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - var rootNode = tree[0]; - var to = rootNode.Children[0]; - var then = rootNode.Children[1]; + var rootNode = tree[0]; + var to = rootNode.Children[0]; + var then = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then); + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then); - ClickContainer(fromContainer, KeyModifiers.None); - ClickContainer(toContainer, KeyModifiers.Shift); + ClickContainer(fromContainer, KeyModifiers.None); + ClickContainer(toContainer, KeyModifiers.Shift); - Assert.Equal(2, target.Selection.SelectedItems.Count); + Assert.Equal(2, target.Selection.SelectedItems.Count); - _mouse.Click(thenContainer, MouseButton.Right); + _mouse.Click(thenContainer, MouseButton.Right); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); + } } [Fact] public void Shift_Right_Click_Should_Not_Select_Multiple() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var rootNode = tree[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - _mouse.Click(fromContainer); - _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift); + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Shift); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); + } } [Fact] public void Ctrl_Right_Click_Should_Not_Select_Multiple() { - var tree = CreateTestTreeData(); - var target = new TreeView + using (Application()) { - Template = CreateTreeViewTemplate(), - Items = tree, - SelectionMode = SelectionMode.Multiple, - }; + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; - var visualRoot = new TestRoot(); - visualRoot.Child = target; + var visualRoot = new TestRoot(); + visualRoot.Child = target; - CreateNodeDataTemplate(target); - ApplyTemplates(target); - target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); - var rootNode = tree[0]; - var from = rootNode.Children[0]; - var to = rootNode.Children[1]; - var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); - var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var rootNode = tree[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - _mouse.Click(fromContainer); - _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control); + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: KeyModifiers.Control); - Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); + } } [Fact] @@ -944,7 +980,7 @@ namespace Avalonia.Controls.UnitTests public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() { /// Issue #2980. - using (UnitTestApplication.Start(TestServices.RealStyler)) + using (Application()) { var target = new DerivedTreeView { @@ -1183,12 +1219,12 @@ namespace Avalonia.Controls.UnitTests } } - void ClickContainer(IControl container, KeyModifiers modifiers) + private void ClickContainer(IControl container, KeyModifiers modifiers) { _mouse.Click(container, modifiers: modifiers); } - void AssertChildrenSelected(TreeView treeView, Node rootNode) + private void AssertChildrenSelected(TreeView treeView, Node rootNode) { foreach (var child in rootNode.Children) { @@ -1198,6 +1234,16 @@ namespace Avalonia.Controls.UnitTests } } + private IDisposable Application() + { + return UnitTestApplication.Start( + TestServices.MockThreadingInterface.With( + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice(), + keyboardNavigation: new KeyboardNavigationHandler(), + inputManager: new InputManager())); + } + private class Node : NotifyingBase { private IAvaloniaList _children; From a3d3690470aa6787a5c2531dab7c0208df6373ea Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 May 2020 18:20:19 +0200 Subject: [PATCH 011/113] Use SelectionModel for DevTools tree. --- .../Diagnostics/ViewModels/TreeNode.cs | 38 ++++++++++++++++++- .../ViewModels/TreePageViewModel.cs | 24 +++++++++--- .../Diagnostics/Views/TreePageView.xaml | 2 +- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index c8a9da600d..aa27538abc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -1,11 +1,11 @@ using System; +using System.Collections.Generic; using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; -using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels @@ -82,5 +82,41 @@ namespace Avalonia.Diagnostics.ViewModels get; private set; } + + public IndexPath Index + { + get + { + var indices = new List(); + var child = this; + var parent = Parent; + + while (parent is object) + { + indices.Add(IndexOf(parent.Children, child)); + child = child.Parent; + parent = parent.Parent; + } + + indices.Add(0); + indices.Reverse(); + return new IndexPath(indices); + } + } + + private static int IndexOf(IReadOnlyList collection, TreeNode item) + { + var count = collection.Count; + + for (var i = 0; i < count; ++i) + { + if (collection[i] == item) + { + return i; + } + } + + throw new AvaloniaInternalException("TreeNode was not present in parent Children collection."); + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index 26b5fe2524..4496cc0a1c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -6,28 +6,40 @@ namespace Avalonia.Diagnostics.ViewModels { internal class TreePageViewModel : ViewModelBase, IDisposable { - private TreeNode _selected; + private TreeNode _selectedNode; private ControlDetailsViewModel _details; private string _propertyFilter; public TreePageViewModel(TreeNode[] nodes) { Nodes = nodes; - } + Selection = new SelectionModel + { + SingleSelect = true, + Source = Nodes + }; + + Selection.SelectionChanged += (s, e) => + { + SelectedNode = (TreeNode)Selection.SelectedItem; + }; + } public TreeNode[] Nodes { get; protected set; } + public SelectionModel Selection { get; } + public TreeNode SelectedNode { - get => _selected; - set + get => _selectedNode; + private set { if (Details != null) { _propertyFilter = Details.PropertyFilter; } - if (RaiseAndSetIfChanged(ref _selected, value)) + if (RaiseAndSetIfChanged(ref _selectedNode, value)) { Details = value != null ? new ControlDetailsViewModel(value.Visual, _propertyFilter) : @@ -83,7 +95,7 @@ namespace Avalonia.Diagnostics.ViewModels if (node != null) { - SelectedNode = node; + Selection.SelectedIndex = node.Index; ExpandNode(node.Parent); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml index a1e6ca7d37..4ddb320175 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml @@ -6,7 +6,7 @@ + Selection="{Binding Selection}"> From d6cce80809ad8303cc48b2fd827d9b1c93f65f5f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 May 2020 18:20:42 +0200 Subject: [PATCH 012/113] Add clarification. --- src/Avalonia.Controls/TreeView.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 1e66bf4d69..a91655855c 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -119,9 +119,10 @@ namespace Avalonia.Controls /// /// Gets or sets the selected item. /// - /// - /// Gets or sets the selected item. - /// + /// + /// Note that setting this property only currently works if the item is expanded to be visible. + /// To select non-expanded nodes use `Selection.SelectedIndex`. + /// public object SelectedItem { get => Selection.SelectedItem; From 229c8b7f12762ce4f37c241c13c15e50bde83c1b Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 29 May 2020 00:36:15 +0200 Subject: [PATCH 013/113] Benchmark and simple optimization for INPC accessor. --- .../Plugins/InpcPropertyAccessorPlugin.cs | 8 ++- .../Data/PropertyAccessorBenchmarks.cs | 57 +++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index f2ed86d2aa..84ef0fb695 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -1,7 +1,5 @@ using System; using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; using System.Reflection; using Avalonia.Utilities; @@ -31,7 +29,11 @@ namespace Avalonia.Data.Core.Plugins Contract.Requires(propertyName != null); reference.TryGetTarget(out object instance); - var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(x => x.Name == propertyName); + + const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | + BindingFlags.Static | BindingFlags.Instance; + + var p = instance.GetType().GetProperty(propertyName, bindingFlags); if (p != null) { diff --git a/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs b/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs new file mode 100644 index 0000000000..5c2502422a --- /dev/null +++ b/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs @@ -0,0 +1,57 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Avalonia.Data.Core.Plugins; +using BenchmarkDotNet.Attributes; +using JetBrains.Annotations; + +namespace Avalonia.Benchmarks.Data +{ + [MemoryDiagnoser, InProcess] + public class PropertyAccessorBenchmarks + { + private readonly InpcPropertyAccessorPlugin _plugin = new InpcPropertyAccessorPlugin(); + private readonly TestObject _targetStrongRef = new TestObject(); + private readonly WeakReference _targetWeakRef; + + public PropertyAccessorBenchmarks() + { + _targetWeakRef = new WeakReference(_targetStrongRef); + } + + [Benchmark] + public void InpcAccessor() + { + _plugin.Start(_targetWeakRef, nameof(TestObject.Test)); + } + + private class TestObject : INotifyPropertyChanged + { + private string _test; + + public string Test + { + get => _test; + set + { + if (_test == value) + { + return; + } + + _test = value; + + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + } +} From aa934c81f6f613935683615e25073ed45af93a93 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 28 May 2020 11:39:59 +0800 Subject: [PATCH 014/113] Porting resources --- .../Accents/FluentBaseDark.xaml | 28 +++++++++- .../Accents/FluentBaseLight.xaml | 28 +++++++++- .../Accents/FluentControlResourcesDark.xaml | 55 ++++++++++++++++++- .../Accents/FluentControlResourcesLight.xaml | 55 ++++++++++++++++++- 4 files changed, 154 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml index 292870c3cf..e8e4c96786 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml @@ -1,6 +1,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml index f734e1eff8..b693df38f4 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -1,6 +1,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 3f9dc68383..1de481b40b 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -1,6 +1,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 46b8f4422c..1b755c133d 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -1,6 +1,6 @@ - From cbc94cd708662b603572a91dcd639cbbf050235d Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 28 May 2020 12:04:45 +0800 Subject: [PATCH 015/113] WIP --- src/Avalonia.Themes.Default/Slider.xaml | 329 ++++++++++++++++++------ 1 file changed, 246 insertions(+), 83 deletions(-) diff --git a/src/Avalonia.Themes.Default/Slider.xaml b/src/Avalonia.Themes.Default/Slider.xaml index b21cbf3650..f9857be906 100644 --- a/src/Avalonia.Themes.Default/Slider.xaml +++ b/src/Avalonia.Themes.Default/Slider.xaml @@ -1,93 +1,256 @@ - - - + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + From e4ff69e0c724fffc4455a0f940ce1b9b5a71c3da Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 28 May 2020 12:12:26 +0800 Subject: [PATCH 016/113] swap --- src/Avalonia.Themes.Default/Slider.xaml | 331 ++++++------------------ src/Avalonia.Themes.Fluent/Slider.xaml | 329 +++++++++++++++++------ 2 files changed, 330 insertions(+), 330 deletions(-) diff --git a/src/Avalonia.Themes.Default/Slider.xaml b/src/Avalonia.Themes.Default/Slider.xaml index f9857be906..9196663dd0 100644 --- a/src/Avalonia.Themes.Default/Slider.xaml +++ b/src/Avalonia.Themes.Default/Slider.xaml @@ -1,256 +1,93 @@ - - - 0,0,0,4 - 15 - 15 - 32 - 32 - 10 - 20 - 20 - 20 - 20 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml index b21cbf3650..f9857be906 100644 --- a/src/Avalonia.Themes.Fluent/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -1,93 +1,256 @@ - - - + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + From b689ba3c6d23b6f7710f944196a74c04a0b47ab0 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 28 May 2020 14:21:04 +0800 Subject: [PATCH 017/113] port visual states --- src/Avalonia.Themes.Fluent/Slider.xaml | 266 +++++++++++-------------- 1 file changed, 116 insertions(+), 150 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml index f9857be906..014095b924 100644 --- a/src/Avalonia.Themes.Fluent/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -1,5 +1,4 @@ - + 0,0,0,4 15 @@ -12,8 +11,7 @@ 20 20 - - + - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - + - - - - - + + - + - - - - - + + - - + + - - - - - - - - - - - From f255f2685e0eba530b4b0f8bcc0b77eb0c94ce49 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 28 May 2020 15:13:32 +0800 Subject: [PATCH 018/113] try making slider work --- src/Avalonia.Controls/Slider.cs | 15 +- src/Avalonia.Themes.Fluent/Slider.xaml | 207 +++++++++--------- src/Avalonia.Visuals/Media/Color.cs | 2 + .../Media/ColorToSolidColorBrushConverter.cs | 36 +++ 4 files changed, 148 insertions(+), 112 deletions(-) create mode 100644 src/Avalonia.Visuals/Media/ColorToSolidColorBrushConverter.cs diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index e92c8faf20..9faf77d445 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -94,9 +94,18 @@ namespace Avalonia.Controls _increaseButton.Click -= IncreaseClick; } - _decreaseButton = e.NameScope.Find - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - From 3b48112e1d3f44beaf0df52eb738554e3bd81e57 Mon Sep 17 00:00:00 2001 From: amwx Date: Fri, 29 May 2020 04:24:07 -0500 Subject: [PATCH 029/113] Fluent Calendar Style --- src/Avalonia.Themes.Fluent/Calendar.xaml | 2 +- src/Avalonia.Themes.Fluent/CalendarButton.xaml | 9 ++++++--- src/Avalonia.Themes.Fluent/CalendarDayButton.xaml | 14 ++++++-------- src/Avalonia.Themes.Fluent/CalendarItem.xaml | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Calendar.xaml b/src/Avalonia.Themes.Fluent/Calendar.xaml index 2fbc705d83..6ae334dbae 100644 --- a/src/Avalonia.Themes.Fluent/Calendar.xaml +++ b/src/Avalonia.Themes.Fluent/Calendar.xaml @@ -5,7 +5,7 @@ // All other rights reserved. --> - + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml index 87cc0d38a0..ba765dcb74 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -1,4 +1,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index f09569e613..70a9fcacef 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -1,6 +1,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 97497495be..90179184f5 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -1,6 +1,6 @@ - From 6005d3fdbfaaa6fab47cf471faff348d72420dd4 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 28 May 2020 12:04:45 +0800 Subject: [PATCH 033/113] WIP --- src/Avalonia.Themes.Default/Slider.xaml | 329 ++++++++++++++++++------ 1 file changed, 246 insertions(+), 83 deletions(-) diff --git a/src/Avalonia.Themes.Default/Slider.xaml b/src/Avalonia.Themes.Default/Slider.xaml index b21cbf3650..f9857be906 100644 --- a/src/Avalonia.Themes.Default/Slider.xaml +++ b/src/Avalonia.Themes.Default/Slider.xaml @@ -1,93 +1,256 @@ - - - + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + From 703ff980949fe896284e5dbdf5e163719a6f6a31 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 28 May 2020 12:12:26 +0800 Subject: [PATCH 034/113] swap --- src/Avalonia.Themes.Default/Slider.xaml | 331 ++++++------------------ src/Avalonia.Themes.Fluent/Slider.xaml | 329 +++++++++++++++++------ 2 files changed, 330 insertions(+), 330 deletions(-) diff --git a/src/Avalonia.Themes.Default/Slider.xaml b/src/Avalonia.Themes.Default/Slider.xaml index f9857be906..9196663dd0 100644 --- a/src/Avalonia.Themes.Default/Slider.xaml +++ b/src/Avalonia.Themes.Default/Slider.xaml @@ -1,256 +1,93 @@ - - - 0,0,0,4 - 15 - 15 - 32 - 32 - 10 - 20 - 20 - 20 - 20 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml index b21cbf3650..f9857be906 100644 --- a/src/Avalonia.Themes.Fluent/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -1,93 +1,256 @@ - - - + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + From 4c7bcc09cfa542d974230d7e45bcc6bdca74ee62 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 28 May 2020 14:21:04 +0800 Subject: [PATCH 035/113] port visual states --- src/Avalonia.Themes.Fluent/Slider.xaml | 266 +++++++++++-------------- 1 file changed, 116 insertions(+), 150 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml index f9857be906..014095b924 100644 --- a/src/Avalonia.Themes.Fluent/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -1,5 +1,4 @@ - + 0,0,0,4 15 @@ -12,8 +11,7 @@ 20 20 - - + - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - + - - - - - + + - + - - - - - + + - - + + - - - - - - - - - - - From c577f6a5bdc613662ee5909b689921ebcb774129 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 28 May 2020 15:13:32 +0800 Subject: [PATCH 036/113] try making slider work --- src/Avalonia.Controls/Slider.cs | 15 +- src/Avalonia.Themes.Fluent/Slider.xaml | 207 +++++++++--------- src/Avalonia.Visuals/Media/Color.cs | 2 + .../Media/ColorToSolidColorBrushConverter.cs | 36 +++ 4 files changed, 148 insertions(+), 112 deletions(-) create mode 100644 src/Avalonia.Visuals/Media/ColorToSolidColorBrushConverter.cs diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index e92c8faf20..9faf77d445 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -94,9 +94,18 @@ namespace Avalonia.Controls _increaseButton.Click -= IncreaseClick; } - _decreaseButton = e.NameScope.Find - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - From 5065e6c072020860312c01053365105759c5e7eb Mon Sep 17 00:00:00 2001 From: amwx Date: Fri, 29 May 2020 04:24:07 -0500 Subject: [PATCH 048/113] Fluent Calendar Style --- src/Avalonia.Themes.Fluent/Calendar.xaml | 2 +- src/Avalonia.Themes.Fluent/CalendarButton.xaml | 9 ++++++--- src/Avalonia.Themes.Fluent/CalendarDayButton.xaml | 14 ++++++-------- src/Avalonia.Themes.Fluent/CalendarItem.xaml | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Calendar.xaml b/src/Avalonia.Themes.Fluent/Calendar.xaml index 2fbc705d83..6ae334dbae 100644 --- a/src/Avalonia.Themes.Fluent/Calendar.xaml +++ b/src/Avalonia.Themes.Fluent/Calendar.xaml @@ -5,7 +5,7 @@ // All other rights reserved. --> - + --> + + diff --git a/src/Avalonia.Themes.Fluent/CalendarDayButton.xaml b/src/Avalonia.Themes.Fluent/CalendarDayButton.xaml index 4f6f5951d2..ff72044c93 100644 --- a/src/Avalonia.Themes.Fluent/CalendarDayButton.xaml +++ b/src/Avalonia.Themes.Fluent/CalendarDayButton.xaml @@ -19,12 +19,14 @@ + + + BorderThickness="0" ClipToBounds="True"> + BorderBrush="{TemplateBinding BorderBrush}"/> - + + + --> + diff --git a/src/Avalonia.Themes.Fluent/CalendarItem.xaml b/src/Avalonia.Themes.Fluent/CalendarItem.xaml index 7e17ee7075..df17da84dc 100644 --- a/src/Avalonia.Themes.Fluent/CalendarItem.xaml +++ b/src/Avalonia.Themes.Fluent/CalendarItem.xaml @@ -61,6 +61,9 @@ + + + + + + @@ -109,22 +109,22 @@ - - - + + + + + + + - - + + From cf7a9122b45d0b8d395948426808f8cc7b6a1a8d Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 30 May 2020 19:06:14 +0800 Subject: [PATCH 052/113] Fix merge conflicts --- src/Avalonia.Controls/ListBoxItem.cs | 30 +-- src/Avalonia.Controls/Mixins/PressedMixin.cs | 37 ++++ src/Avalonia.Controls/Platform/IPopupImpl.cs | 2 + src/Avalonia.Controls/Primitives/Popup.cs | 21 +- src/Avalonia.Controls/Primitives/PopupRoot.cs | 6 +- src/Avalonia.Controls/Slider.cs | 8 +- .../Utils/BorderRenderHelper.cs | 9 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 4 + src/Avalonia.Input/NavigationDirection.cs | 2 +- src/Avalonia.Native/PopupImpl.cs | 5 + .../Accents/BaseDark.xaml | 12 +- .../Accents/BaseLight.xaml | 12 +- .../Accents/FluentBaseDark.xaml | 154 ++++++++----- .../Accents/FluentBaseLight.xaml | 153 ++++++++----- .../Accents/FluentControlResourcesDark.xaml | 149 +++++++++---- .../Accents/FluentControlResourcesLight.xaml | 149 +++++++++---- .../AutoCompleteBox.xaml | 68 ++++-- src/Avalonia.Themes.Fluent/PopupRoot.xaml | 38 ++-- src/Avalonia.Themes.Fluent/RadioButton.xaml | 207 ++++++++++++++---- src/Avalonia.Themes.Fluent/Slider.xaml | 12 +- src/Avalonia.X11/X11Window.cs | 4 + src/Windows/Avalonia.Win32/PopupImpl.cs | 34 ++- .../Mixins/PressedMixinTests.cs | 37 ++++ 23 files changed, 806 insertions(+), 347 deletions(-) create mode 100644 src/Avalonia.Controls/Mixins/PressedMixin.cs create mode 100644 tests/Avalonia.Controls.UnitTests/Mixins/PressedMixinTests.cs diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index 46a7ccec0b..e04c79987f 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -20,6 +20,7 @@ namespace Avalonia.Controls static ListBoxItem() { SelectableMixin.Attach(IsSelectedProperty); + PressedMixin.Attach(); FocusableProperty.OverrideDefaultValue(true); } @@ -31,34 +32,5 @@ namespace Avalonia.Controls get { return GetValue(IsSelectedProperty); } set { SetValue(IsSelectedProperty, value); } } - - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - base.OnPointerPressed(e); - - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) - { - UpdatePseudoClasses(true); - } - } - - protected override void OnPointerReleased(PointerReleasedEventArgs e) - { - base.OnPointerReleased(e); - - UpdatePseudoClasses(false); - } - - protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) - { - base.OnPointerCaptureLost(e); - - UpdatePseudoClasses(false); - } - - private void UpdatePseudoClasses(bool isPressed) - { - PseudoClasses.Set(":pressed", isPressed); - } } } diff --git a/src/Avalonia.Controls/Mixins/PressedMixin.cs b/src/Avalonia.Controls/Mixins/PressedMixin.cs new file mode 100644 index 0000000000..4c5792b4a0 --- /dev/null +++ b/src/Avalonia.Controls/Mixins/PressedMixin.cs @@ -0,0 +1,37 @@ +using Avalonia.Interactivity; +using Avalonia.Input; + +namespace Avalonia.Controls.Mixins +{ + /// + /// Adds pressed functionality to control classes. + /// + /// Adds the ':pressed' class when the item is pressed. + /// + public static class PressedMixin + { + /// + /// Initializes a new instance of the class. + /// + /// The control type. + public static void Attach() where TControl : Control + { + InputElement.PointerPressedEvent.AddClassHandler((x, e) => HandlePointerPressed(x, e), RoutingStrategies.Tunnel); + InputElement.PointerReleasedEvent.AddClassHandler((x, e) => HandlePointerReleased(x), RoutingStrategies.Tunnel); + InputElement.PointerCaptureLostEvent.AddClassHandler((x, e) => HandlePointerReleased(x), RoutingStrategies.Tunnel); + } + + private static void HandlePointerPressed(TControl sender, PointerPressedEventArgs e) where TControl : Control + { + if (e.GetCurrentPoint(sender).Properties.IsLeftButtonPressed) + { + ((IPseudoClasses)sender.Classes).Set(":pressed", true); + } + } + + private static void HandlePointerReleased(TControl sender) where TControl : Control + { + ((IPseudoClasses)sender.Classes).Set(":pressed", false); + } + } +} diff --git a/src/Avalonia.Controls/Platform/IPopupImpl.cs b/src/Avalonia.Controls/Platform/IPopupImpl.cs index 3944695f04..477d5fab43 100644 --- a/src/Avalonia.Controls/Platform/IPopupImpl.cs +++ b/src/Avalonia.Controls/Platform/IPopupImpl.cs @@ -8,5 +8,7 @@ namespace Avalonia.Platform public interface IPopupImpl : IWindowBaseImpl { IPopupPositioner PopupPositioner { get; } + + void SetWindowManagerAddShadowHint(bool enabled); } } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 66f2153b6c..49315e1b25 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -19,6 +19,9 @@ namespace Avalonia.Controls.Primitives /// public class Popup : Control, IVisualTreeHost { + public static readonly StyledProperty WindowManagerAddShadowHintProperty = + AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), true); + /// /// Defines the property. /// @@ -89,7 +92,7 @@ namespace Avalonia.Controls.Primitives { IsHitTestVisibleProperty.OverrideDefaultValue(false); ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e)); - IsOpenProperty.Changed.AddClassHandler((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs)e)); + IsOpenProperty.Changed.AddClassHandler((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs)e)); } /// @@ -104,6 +107,12 @@ namespace Avalonia.Controls.Primitives public IPopupHost? Host => _openState?.PopupHost; + public bool WindowManagerAddShadowHint + { + get { return GetValue(WindowManagerAddShadowHintProperty); } + set { SetValue(WindowManagerAddShadowHintProperty, value); } + } + /// /// Gets or sets the control to display in the popup. /// @@ -293,6 +302,8 @@ namespace Avalonia.Controls.Primitives _openState = new PopupOpenState(topLevel, popupHost, cleanupPopup); + WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint); + popupHost.Show(); using (BeginIgnoringIsOpen()) @@ -332,6 +343,14 @@ namespace Avalonia.Controls.Primitives return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler)); } + private void WindowManagerAddShadowHintChanged(IPopupHost host, bool hint) + { + if(host is PopupRoot pr) + { + pr.PlatformImpl.SetWindowManagerAddShadowHint(hint); + } + } + /// /// Called when the property changes. /// diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 788fe03162..aab7a68795 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -17,14 +17,14 @@ namespace Avalonia.Controls.Primitives public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost { private readonly TopLevel _parent; - private PopupPositionerParameters _positionerParameters; + private PopupPositionerParameters _positionerParameters; /// /// Initializes static members of the class. /// static PopupRoot() { - BackgroundProperty.OverrideDefaultValue(typeof(PopupRoot), Brushes.White); + BackgroundProperty.OverrideDefaultValue(typeof(PopupRoot), Brushes.White); } /// @@ -53,7 +53,7 @@ namespace Avalonia.Controls.Primitives /// Gets the platform-specific window implementation. /// [CanBeNull] - public new IPopupImpl PlatformImpl => (IPopupImpl)base.PlatformImpl; + public new IPopupImpl PlatformImpl => (IPopupImpl)base.PlatformImpl; /// /// Gets the parent control in the event route. diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index b218c003c2..2db7c5cb83 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -1,6 +1,6 @@ using System; +using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; -using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; @@ -40,13 +40,11 @@ namespace Avalonia.Controls /// static Slider() { + PressedMixin.Attach(); OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal); Thumb.DragStartedEvent.AddClassHandler((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble); Thumb.DragDeltaEvent.AddClassHandler((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble); - Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble); - PointerPressedEvent.AddClassHandler((x, e) => x.HandlePointerPressed(e), RoutingStrategies.Tunnel, true); - PointerReleasedEvent.AddClassHandler((x, e) => x.HandlePointerReleased(), RoutingStrategies.Tunnel, true); - PointerCaptureLostEvent.AddClassHandler((x, e) => x.HandlePointerReleased(), RoutingStrategies.Tunnel, true); + Thumb.DragCompletedEvent.AddClassHandler((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble); } /// diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 586fe4d341..51eb6edbea 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -118,12 +118,11 @@ namespace Avalonia.Controls.Utils pen = new Pen(borderBrush, borderThickness); } - var rrect = new RoundedRect(new Rect(_size), _cornerRadius.TopLeft, _cornerRadius.TopRight, - _cornerRadius.BottomRight, _cornerRadius.BottomLeft); + var rect = new Rect(_size); if (Math.Abs(borderThickness) > double.Epsilon) - { - rrect = rrect.Deflate(borderThickness * 0.5, borderThickness * 0.5); - } + rect = rect.Deflate(borderThickness * 0.5); + var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight, + _cornerRadius.BottomRight, _cornerRadius.BottomLeft); context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadows); } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index a9665b1519..c2565cc59c 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -146,6 +146,10 @@ namespace Avalonia.DesignerSupport.Remote public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } + public void SetWindowManagerAddShadowHint(bool enabled) + { + } + public WindowTransparencyLevel TransparencyLevel { get; private set; } } diff --git a/src/Avalonia.Input/NavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs index 34bedac141..9b9af0b0a6 100644 --- a/src/Avalonia.Input/NavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -102,7 +102,7 @@ namespace Avalonia.Input switch (key) { case Key.Tab: - return (modifiers & KeyModifiers.Shift) != 0 ? + return (modifiers & KeyModifiers.Shift) == 0 ? NavigationDirection.Next : NavigationDirection.Previous; case Key.Up: return NavigationDirection.Up; diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index e4ee293757..c41be1723b 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -59,6 +59,11 @@ namespace Avalonia.Native } public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, _glFeature, this); + + public void SetWindowManagerAddShadowHint(bool enabled) + { + } + public IPopupPositioner PopupPositioner { get; } } } diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml index 7ce6c5a19f..bf169400f0 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml @@ -139,6 +139,7 @@ + False @@ -159,8 +160,7 @@ 24 40 3 - 374 - 0 + 0 2 0 21 @@ -258,10 +258,10 @@ 0,0,0,0 0,0,0,0 3,0,3,0 - 1 - 0,2,0,2 - -1,0,-1,0 - 12,11,0,13 + 1 + 0,2,0,2 + -1,0,-1,0 + 12,11,0,13 2 2 2 diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml index 026edca4f9..b32c816851 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml @@ -139,6 +139,7 @@ + False @@ -158,8 +159,7 @@ 24 40 3 - 374 - 0 + 0 2 0 21 @@ -257,10 +257,10 @@ 0,0,0,0 0,0,0,0 3,0,3,0 - 1 - 0,2,0,2 - -1,0,-1,0 - 12,11,0,13 + 1 + 0,2,0,2 + -1,0,-1,0 + 12,11,0,13 2 2 2 diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml index ab4806d6cd..865f9e8759 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml @@ -1,5 +1,5 @@ - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml index 1d3e415e90..ae69e82ea5 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -1,5 +1,5 @@ - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index b395e3983f..3a992a24f9 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -1,5 +1,5 @@ - diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index e1cc0302c7..575c3b33ae 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -1,5 +1,5 @@ - diff --git a/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml b/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml index 788b60892b..52bacdb36e 100644 --- a/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml @@ -1,37 +1,69 @@ - + + + + + + Alabama + Alaska + Arizona + Arkansas + California + Colorado + Connecticut + Delaware + + + + + diff --git a/src/Avalonia.Themes.Fluent/PopupRoot.xaml b/src/Avalonia.Themes.Fluent/PopupRoot.xaml index 9af4f5a910..25c71e3493 100644 --- a/src/Avalonia.Themes.Fluent/PopupRoot.xaml +++ b/src/Avalonia.Themes.Fluent/PopupRoot.xaml @@ -1,17 +1,21 @@ - + + + diff --git a/src/Avalonia.Themes.Fluent/RadioButton.xaml b/src/Avalonia.Themes.Fluent/RadioButton.xaml index 4cdb116cdd..23bcfc616a 100644 --- a/src/Avalonia.Themes.Fluent/RadioButton.xaml +++ b/src/Avalonia.Themes.Fluent/RadioButton.xaml @@ -1,60 +1,173 @@ + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml index fff8525ebd..07dc318961 100644 --- a/src/Avalonia.Themes.Fluent/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -48,9 +48,9 @@ - - - + + + @@ -109,9 +109,9 @@ - - - + + + diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 4899f0efc0..1d41fe4bdd 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -1098,6 +1098,10 @@ namespace Avalonia.X11 public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => _transparencyHelper.SetTransparencyRequest(transparencyLevel); + public void SetWindowManagerAddShadowHint(bool enabled) + { + } + public WindowTransparencyLevel TransparencyLevel => _transparencyHelper.CurrentLevel; } } diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 504cefb9b3..efcf1ea674 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -7,6 +7,8 @@ namespace Avalonia.Win32 { class PopupImpl : WindowImpl, IPopupImpl { + private bool _dropShadowHint = true; + public override void Show() { UnmanagedMethods.ShowWindow(Handle.Handle, UnmanagedMethods.ShowWindowCommand.ShowNoActivate); @@ -36,11 +38,7 @@ namespace Avalonia.Win32 IntPtr.Zero, IntPtr.Zero); - var classes = (int)UnmanagedMethods.GetClassLongPtr(result, (int)UnmanagedMethods.ClassLongIndex.GCL_STYLE); - - classes |= (int)UnmanagedMethods.ClassStyles.CS_DROPSHADOW; - - UnmanagedMethods.SetClassLong(result, UnmanagedMethods.ClassLongIndex.GCL_STYLE, new IntPtr(classes)); + EnableBoxShadow(result, _dropShadowHint); return result; } @@ -68,6 +66,32 @@ namespace Avalonia.Win32 //TODO: We ignore the scaling override for now } + private void EnableBoxShadow (IntPtr hwnd, bool enabled) + { + var classes = (int)UnmanagedMethods.GetClassLongPtr(hwnd, (int)UnmanagedMethods.ClassLongIndex.GCL_STYLE); + + if (enabled) + { + classes |= (int)UnmanagedMethods.ClassStyles.CS_DROPSHADOW; + } + else + { + classes &= ~(int)UnmanagedMethods.ClassStyles.CS_DROPSHADOW; + } + + UnmanagedMethods.SetClassLong(hwnd, UnmanagedMethods.ClassLongIndex.GCL_STYLE, new IntPtr(classes)); + } + + public void SetWindowManagerAddShadowHint(bool enabled) + { + _dropShadowHint = enabled; + + if (Handle != null) + { + EnableBoxShadow(Handle.Handle, enabled); + } + } + public IPopupPositioner PopupPositioner { get; } } } diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/PressedMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/PressedMixinTests.cs new file mode 100644 index 0000000000..0ff1f40121 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Mixins/PressedMixinTests.cs @@ -0,0 +1,37 @@ +using Avalonia.Controls.Mixins; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Mixins +{ + public class PressedMixinTests + { + private MouseTestHelper _mouse = new MouseTestHelper(); + + [Fact] + public void Selected_Class_Should_Not_Initially_Be_Added() + { + var target = new TestControl(); + + Assert.Empty(target.Classes); + } + + [Fact] + public void Setting_IsSelected_Should_Add_Selected_Class() + { + var target = new TestControl(); + + _mouse.Down(target); + + Assert.Equal(new[] { ":pressed" }, target.Classes); + } + + private class TestControl : Control + { + static TestControl() + { + PressedMixin.Attach(); + } + } + } +} From a43b080e36d8f10bec77abd7ce956a05cc1a62a6 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 30 May 2020 19:16:14 +0800 Subject: [PATCH 053/113] add TickBar styles back --- src/Avalonia.Themes.Fluent/Slider.xaml | 44 ++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml index 75f64952ed..ac6ecf419a 100644 --- a/src/Avalonia.Themes.Fluent/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -58,11 +58,11 @@ - + - + - + @@ -119,11 +119,11 @@ - + - + - + @@ -175,6 +175,22 @@ + + + + + + + + + + + + + + + + From abe3914258aad5cc7cd4b97bd619761830f0584b Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 30 May 2020 19:22:57 +0800 Subject: [PATCH 054/113] fix tickbar fill --- src/Avalonia.Themes.Fluent/Slider.xaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml index ac6ecf419a..77b5f9568d 100644 --- a/src/Avalonia.Themes.Fluent/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -176,19 +176,19 @@ @@ -209,19 +209,19 @@ + + - - - - - - - - - - - - - - - - + From 5f421f33788a8d5343b4064b459986fea720444f Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 30 May 2020 21:23:15 +0800 Subject: [PATCH 057/113] fix reservedspace --- src/Avalonia.Controls/TickBar.cs | 18 ------------------ src/Avalonia.Themes.Fluent/Slider.xaml | 13 ++++++++++--- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Controls/TickBar.cs b/src/Avalonia.Controls/TickBar.cs index aa140a197b..b6637643e8 100644 --- a/src/Avalonia.Controls/TickBar.cs +++ b/src/Avalonia.Controls/TickBar.cs @@ -36,24 +36,6 @@ namespace Avalonia.Controls Bottom, // NOTE: if you add or remove any values in this enum, be sure to update TickBar.IsValidTickBarPlacement() - }; - - public static class TickBarConverter - { - public static readonly IValueConverter ReservedSpaceBind = - new FuncValueConverter(x => - { - if (x is null) - { - - } - if (x is Rect y) - { - return y; - } - return new Rect(); - }); - } /// diff --git a/src/Avalonia.Themes.Fluent/Slider.xaml b/src/Avalonia.Themes.Fluent/Slider.xaml index 648845f4b7..7afbac5f63 100644 --- a/src/Avalonia.Themes.Fluent/Slider.xaml +++ b/src/Avalonia.Themes.Fluent/Slider.xaml @@ -50,8 +50,12 @@ + + + - @@ -111,8 +115,12 @@ + + + - @@ -172,7 +180,6 @@ - From 546e52be154485d580b4c4f17f382cc92f7ae5de Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 13:31:04 -0300 Subject: [PATCH 058/113] remove unused code. --- src/Avalonia.Controls/Slider.cs | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index ba183904e3..01a23c2582 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -98,7 +98,7 @@ namespace Avalonia.Controls _decreaseButton = e.NameScope.Find