From 4e4c1918ab6dc7cf9b4979dd5b4dde0fc8afd364 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 1 Jul 2019 22:00:19 +0200 Subject: [PATCH 1/8] Added failing test for #2660. And a passing test too. Also added some useful methods to `TreeView` to help with this. --- src/Avalonia.Controls/TreeView.cs | 44 +++++++++++- .../TreeViewTests.cs | 67 ++++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 888f4a2013..e186f08561 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -164,6 +164,48 @@ namespace Avalonia.Controls } } + /// + /// Expands the specified all descendent s. + /// + /// The item to expand. + public void ExpandSubTree(TreeViewItem item) + { + item.IsExpanded = true; + + var panel = item.Presenter.Panel; + + if (panel != null) + { + foreach (var child in panel.Children) + { + if (child is TreeViewItem treeViewItem) + { + ExpandSubTree(treeViewItem); + } + } + } + } + + /// + /// Selects all items in the . + /// + /// + /// Note that this method only selects nodes currently visible due to their parent nodes + /// being expanded: it does not expand nodes. + /// + public void SelectAll() + { + SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + } + + /// + /// Deselects all items in the . + /// + public void UnselectAll() + { + SelectedItems.Clear(); + } + /// /// Subscribes to the CollectionChanged event, if any. /// @@ -409,7 +451,7 @@ namespace Avalonia.Controls if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) { - SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + SelectAll(); e.Handled = true; } } diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index b66d6ed11c..8defb353b3 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -11,6 +11,7 @@ using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.UnitTests; using Xunit; @@ -740,6 +741,71 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + target.SelectAll(); + + AssertChildrenSelected(target, tree[0]); + Assert.Equal(5, target.SelectedItems.Count); + + _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); + + Assert.Equal(5, target.SelectedItems.Count); + } + + [Fact] + public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + 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 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, InputModifiers.None); + ClickContainer(toContainer, InputModifiers.Shift); + + Assert.Equal(2, target.SelectedItems.Count); + + _mouse.Click(thenContainer, MouseButton.Right); + + Assert.Equal(1, target.SelectedItems.Count); + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); @@ -874,7 +940,6 @@ namespace Avalonia.Controls.UnitTests } } - private class Node : NotifyingBase { private IAvaloniaList _children; From 67126261813c0ec28ca013287392ffb46e6f166b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 1 Jul 2019 22:25:51 +0200 Subject: [PATCH 2/8] Simplify setting TreeView.SelectedItem. Calling the overload of `SetAndRaise` which takes a callback should not be necessary because that was only needed in `SelectingItemsControl` due to the interaction between `SelectedIndex` and `SelectedItem`. --- src/Avalonia.Controls/TreeView.cs | 39 +++++++++++-------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index e186f08561..4404e46ad9 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -105,32 +105,21 @@ namespace Avalonia.Controls get => _selectedItem; set { - SetAndRaise(SelectedItemProperty, ref _selectedItem, - (object val, ref object backing, Action notifyWrapper) => - { - var old = backing; - backing = val; - - notifyWrapper(() => - RaisePropertyChanged( - SelectedItemProperty, - old, - val)); + SetAndRaise(SelectedItemProperty, ref _selectedItem, value); - if (val != null) - { - if (SelectedItems.Count != 1 || SelectedItems[0] != val) - { - _syncingSelectedItems = true; - SelectSingleItem(val); - _syncingSelectedItems = false; - } - } - else if (SelectedItems.Count > 0) - { - SelectedItems.Clear(); - } - }, value); + if (value != null) + { + if (SelectedItems.Count != 1 || SelectedItems[0] != value) + { + _syncingSelectedItems = true; + SelectSingleItem(value); + _syncingSelectedItems = false; + } + } + else if (SelectedItems.Count > 0) + { + SelectedItems.Clear(); + } } } From f8741ead2c072c87a489ed0697a2dbfbe595c6b4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 08:40:36 +0200 Subject: [PATCH 3/8] Added failing SelectingItemsControl tests. --- .../SelectingItemsControlTests_Multiple.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index a33d97779e..d66acdf9ce 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1026,6 +1026,46 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(1, target.SelectedItems.Count); } + [Fact] + public void Shift_Right_Click_Should_Not_Select_Mutiple() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: InputModifiers.Shift); + + Assert.Equal(1, target.SelectedItems.Count); + } + + [Fact] + public void Ctrl_Right_Click_Should_Not_Select_Mutiple() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: InputModifiers.Control); + + Assert.Equal(1, target.SelectedItems.Count); + } + private IEnumerable SelectedContainers(SelectingItemsControl target) { return target.Presenter.Panel.Children From 07a625d48942b0343c2d98e4de1394f459e156a6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 08:55:17 +0200 Subject: [PATCH 4/8] Added more failing TreeViewTests. --- .../TreeViewTests.cs | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 8defb353b3..bb8466ea3e 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -749,7 +749,6 @@ namespace Avalonia.Controls.UnitTests { Template = CreateTreeViewTemplate(), Items = tree, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; @@ -777,7 +776,6 @@ namespace Avalonia.Controls.UnitTests { Template = CreateTreeViewTemplate(), Items = tree, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; @@ -806,6 +804,66 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, target.SelectedItems.Count); } + [Fact] + public void Shift_Right_Click_Should_Not_Select_Mutiple() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + 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); + + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: InputModifiers.Shift); + + Assert.Equal(1, target.SelectedItems.Count); + } + + [Fact] + public void Ctrl_Right_Click_Should_Not_Select_Mutiple() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + 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); + + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: InputModifiers.Control); + + Assert.Equal(1, target.SelectedItems.Count); + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); From f634e9af81bec1669a9526286965549d34b868c3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 09:09:39 +0200 Subject: [PATCH 5/8] Fix TreeView right-click selection. Fixes #2660. --- src/Avalonia.Controls/TreeView.cs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 4404e46ad9..4514109e12 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -510,7 +510,8 @@ namespace Avalonia.Controls e.Source, true, (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0); + (e.InputModifiers & InputModifiers.Control) != 0, + e.MouseButton == MouseButton.Right); } } @@ -521,11 +522,13 @@ namespace Avalonia.Controls /// Whether the item should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. protected void UpdateSelectionFromContainer( IControl container, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var item = ItemContainerGenerator.Index.ItemFromContainer(container); @@ -546,7 +549,14 @@ namespace Avalonia.Controls var multi = (mode & SelectionMode.Multiple) != 0; var range = multi && selectedContainer != null && rangeModifier; - if (!toggle && !range) + if (rightButton) + { + if (!SelectedItems.Contains(item)) + { + SelectSingleItem(item); + } + } + else if (!toggle && !range) { SelectSingleItem(item); } @@ -715,6 +725,7 @@ namespace Avalonia.Controls /// Whether the container should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. /// /// True if the event originated from a container that belongs to the control; otherwise /// false. @@ -723,13 +734,14 @@ namespace Avalonia.Controls IInteractive eventSource, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var container = GetContainerFromEventSource(eventSource); if (container != null) { - UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier); + UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier, rightButton); return true; } From afa38852cf4dbb45ce9428dc2e84b4a0e4310ca7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 11:41:42 +0200 Subject: [PATCH 6/8] Fix SelectingItemsControl multiple selection. - Allow `SelectAll` regardless of `SelectionMode`: `SelectionMode` should only apply to user-interaction - Don't select multiple on shift/ctrl-right click --- .../Primitives/SelectingItemsControl.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 91a9fa7e40..2c0f67fada 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -469,11 +469,6 @@ namespace Avalonia.Controls.Primitives /// protected void SelectAll() { - if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0) - { - throw new NotSupportedException("Multiple selection is not enabled on this control."); - } - UpdateSelectedItems(() => { _selection.Clear(); @@ -523,7 +518,14 @@ namespace Avalonia.Controls.Primitives var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); var range = multi && rangeModifier; - if (range) + if (rightButton) + { + if (!_selection.Contains(index)) + { + UpdateSelectedItem(index); + } + } + else if (range) { UpdateSelectedItems(() => { @@ -582,7 +584,7 @@ namespace Avalonia.Controls.Primitives } else { - UpdateSelectedItem(index, !(rightButton && _selection.Contains(index))); + UpdateSelectedItem(index); } if (Presenter?.Panel != null) From 0b88151b658cc67cb8a8dd70ffff60e2a55964a2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 18 Jul 2019 15:16:29 +0200 Subject: [PATCH 7/8] Fix tests failing after merge. --- .../Primitives/SelectingItemsControlTests_Multiple.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 972b933bb9..88a4820286 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1100,7 +1100,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; @@ -1120,7 +1120,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; From cae46af2870b9e5fd54c9fe94f1bcb11acc81db4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 19 Jul 2019 00:21:03 +0200 Subject: [PATCH 8/8] Fixed typos. --- .../Primitives/SelectingItemsControlTests_Multiple.cs | 4 ++-- tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 88a4820286..4bcfeb6d03 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1094,7 +1094,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Shift_Right_Click_Should_Not_Select_Mutiple() + public void Shift_Right_Click_Should_Not_Select_Multiple() { var target = new ListBox { @@ -1114,7 +1114,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Ctrl_Right_Click_Should_Not_Select_Mutiple() + public void Ctrl_Right_Click_Should_Not_Select_Multiple() { var target = new ListBox { diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 919b600a1c..5646e86f7a 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -784,7 +784,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Shift_Right_Click_Should_Not_Select_Mutiple() + public void Shift_Right_Click_Should_Not_Select_Multiple() { var tree = CreateTestTreeData(); var target = new TreeView @@ -814,7 +814,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Ctrl_Right_Click_Should_Not_Select_Mutiple() + public void Ctrl_Right_Click_Should_Not_Select_Multiple() { var tree = CreateTestTreeData(); var target = new TreeView