Browse Source

Merge pull request #2702 from AvaloniaUI/fixes/2660-treeview-selection

Some more selection improvements to `TreeView` and `SelectingItemsControl`
pull/2762/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
48370e5a85
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  2. 105
      src/Avalonia.Controls/TreeView.cs
  3. 40
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  4. 125
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

16
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -485,11 +485,6 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
protected void SelectAll() protected void SelectAll()
{ {
if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0)
{
throw new NotSupportedException("Multiple selection is not enabled on this control.");
}
UpdateSelectedItems(() => UpdateSelectedItems(() =>
{ {
_selection.Clear(); _selection.Clear();
@ -539,7 +534,14 @@ namespace Avalonia.Controls.Primitives
var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
var range = multi && rangeModifier; var range = multi && rangeModifier;
if (range) if (rightButton)
{
if (!_selection.Contains(index))
{
UpdateSelectedItem(index);
}
}
else if (range)
{ {
UpdateSelectedItems(() => UpdateSelectedItems(() =>
{ {
@ -598,7 +600,7 @@ namespace Avalonia.Controls.Primitives
} }
else else
{ {
UpdateSelectedItem(index, !(rightButton && _selection.Contains(index))); UpdateSelectedItem(index);
} }
if (Presenter?.Panel != null) if (Presenter?.Panel != null)

105
src/Avalonia.Controls/TreeView.cs

@ -105,32 +105,21 @@ namespace Avalonia.Controls
get => _selectedItem; get => _selectedItem;
set set
{ {
SetAndRaise(SelectedItemProperty, ref _selectedItem, SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
(object val, ref object backing, Action<Action> notifyWrapper) =>
{
var old = backing;
backing = val;
notifyWrapper(() =>
RaisePropertyChanged(
SelectedItemProperty,
old,
val));
if (val != null) if (value != null)
{ {
if (SelectedItems.Count != 1 || SelectedItems[0] != val) if (SelectedItems.Count != 1 || SelectedItems[0] != value)
{ {
_syncingSelectedItems = true; _syncingSelectedItems = true;
SelectSingleItem(val); SelectSingleItem(value);
_syncingSelectedItems = false; _syncingSelectedItems = false;
} }
} }
else if (SelectedItems.Count > 0) else if (SelectedItems.Count > 0)
{ {
SelectedItems.Clear(); SelectedItems.Clear();
} }
}, value);
} }
} }
@ -164,6 +153,48 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Expands the specified <see cref="TreeViewItem"/> all descendent <see cref="TreeViewItem"/>s.
/// </summary>
/// <param name="item">The item to expand.</param>
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);
}
}
}
}
/// <summary>
/// Selects all items in the <see cref="TreeView"/>.
/// </summary>
/// <remarks>
/// Note that this method only selects nodes currently visible due to their parent nodes
/// being expanded: it does not expand nodes.
/// </remarks>
public void SelectAll()
{
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
}
/// <summary>
/// Deselects all items in the <see cref="TreeView"/>.
/// </summary>
public void UnselectAll()
{
SelectedItems.Clear();
}
/// <summary> /// <summary>
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any. /// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary> /// </summary>
@ -409,7 +440,7 @@ namespace Avalonia.Controls
if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
{ {
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); SelectAll();
e.Handled = true; e.Handled = true;
} }
} }
@ -479,7 +510,8 @@ namespace Avalonia.Controls
e.Source, e.Source,
true, true,
(e.InputModifiers & InputModifiers.Shift) != 0, (e.InputModifiers & InputModifiers.Shift) != 0,
(e.InputModifiers & InputModifiers.Control) != 0); (e.InputModifiers & InputModifiers.Control) != 0,
e.MouseButton == MouseButton.Right);
} }
} }
@ -490,11 +522,13 @@ namespace Avalonia.Controls
/// <param name="select">Whether the item should be selected or unselected.</param> /// <param name="select">Whether the item should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param> /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param> /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
protected void UpdateSelectionFromContainer( protected void UpdateSelectionFromContainer(
IControl container, IControl container,
bool select = true, bool select = true,
bool rangeModifier = false, bool rangeModifier = false,
bool toggleModifier = false) bool toggleModifier = false,
bool rightButton = false)
{ {
var item = ItemContainerGenerator.Index.ItemFromContainer(container); var item = ItemContainerGenerator.Index.ItemFromContainer(container);
@ -515,7 +549,14 @@ namespace Avalonia.Controls
var multi = (mode & SelectionMode.Multiple) != 0; var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && selectedContainer != null && rangeModifier; var range = multi && selectedContainer != null && rangeModifier;
if (!toggle && !range) if (rightButton)
{
if (!SelectedItems.Contains(item))
{
SelectSingleItem(item);
}
}
else if (!toggle && !range)
{ {
SelectSingleItem(item); SelectSingleItem(item);
} }
@ -684,6 +725,7 @@ namespace Avalonia.Controls
/// <param name="select">Whether the container should be selected or unselected.</param> /// <param name="select">Whether the container should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param> /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param> /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
/// <returns> /// <returns>
/// True if the event originated from a container that belongs to the control; otherwise /// True if the event originated from a container that belongs to the control; otherwise
/// false. /// false.
@ -692,13 +734,14 @@ namespace Avalonia.Controls
IInteractive eventSource, IInteractive eventSource,
bool select = true, bool select = true,
bool rangeModifier = false, bool rangeModifier = false,
bool toggleModifier = false) bool toggleModifier = false,
bool rightButton = false)
{ {
var container = GetContainerFromEventSource(eventSource); var container = GetContainerFromEventSource(eventSource);
if (container != null) if (container != null)
{ {
UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier); UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier, rightButton);
return true; return true;
} }

40
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -1093,6 +1093,46 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(new[] { items[2], items[3] }, target.SelectedItems); Assert.Equal(new[] { items[2], items[3] }, target.SelectedItems);
} }
[Fact]
public void Shift_Right_Click_Should_Not_Select_Multiple()
{
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz" },
ItemTemplate = new FuncDataTemplate<string>((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_Multiple()
{
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz" },
ItemTemplate = new FuncDataTemplate<string>((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<int> SelectedContainers(SelectingItemsControl target) private IEnumerable<int> SelectedContainers(SelectingItemsControl target)
{ {
return target.Presenter.Panel.Children return target.Presenter.Panel.Children

125
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -11,6 +11,7 @@ using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -719,6 +720,129 @@ 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,
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,
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);
}
[Fact]
public void Shift_Right_Click_Should_Not_Select_Multiple()
{
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_Multiple()
{
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) private void ApplyTemplates(TreeView tree)
{ {
tree.ApplyTemplate(); tree.ApplyTemplate();
@ -853,7 +977,6 @@ namespace Avalonia.Controls.UnitTests
} }
} }
private class Node : NotifyingBase private class Node : NotifyingBase
{ {
private IAvaloniaList<Node> _children; private IAvaloniaList<Node> _children;

Loading…
Cancel
Save