Browse Source

Merge branch 'master' into master

pull/2598/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
a305f77a28
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      src/Avalonia.Controls/ListBox.cs
  2. 644
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  3. 26
      src/Avalonia.Controls/TreeView.cs
  4. 115
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  5. 523
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

13
src/Avalonia.Controls/ListBox.cs

@ -84,6 +84,16 @@ namespace Avalonia.Controls
set { SetValue(VirtualizationModeProperty, value); }
}
/// <summary>
/// Selects all items in the <see cref="ListBox"/>.
/// </summary>
public new void SelectAll() => base.SelectAll();
/// <summary>
/// Deselects all items in the <see cref="ListBox"/>.
/// </summary>
public new void UnselectAll() => base.UnselectAll();
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
@ -118,7 +128,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);
}
}

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

@ -12,6 +12,7 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Logging;
using Avalonia.Styling;
using Avalonia.VisualTree;
@ -103,6 +104,7 @@ namespace Avalonia.Controls.Primitives
private static readonly IList Empty = Array.Empty<object>();
private readonly Selection _selection = new Selection();
private int _selectedIndex = -1;
private object _selectedItem;
private IList _selectedItems;
@ -152,23 +154,8 @@ namespace Avalonia.Controls.Primitives
{
if (_updateCount == 0)
{
SetAndRaise(SelectedIndexProperty, ref _selectedIndex, (int val, ref int backing, Action<Action> notifyWrapper) =>
{
var old = backing;
var effective = (val >= 0 && val < Items?.Cast<object>().Count()) ? val : -1;
if (old != effective)
{
backing = effective;
notifyWrapper(() =>
RaisePropertyChanged(
SelectedIndexProperty,
old,
effective,
BindingPriority.LocalValue));
SelectedItem = ElementAt(Items, effective);
}
}, value);
var effective = (value >= 0 && value < ItemCount) ? value : -1;
UpdateSelectedItem(effective);
}
else
{
@ -192,41 +179,7 @@ namespace Avalonia.Controls.Primitives
{
if (_updateCount == 0)
{
SetAndRaise(SelectedItemProperty, ref _selectedItem, (object val, ref object backing, Action<Action> notifyWrapper) =>
{
var old = backing;
var index = IndexOf(Items, val);
var effective = index != -1 ? val : null;
if (!object.Equals(effective, old))
{
backing = effective;
notifyWrapper(() =>
RaisePropertyChanged(
SelectedItemProperty,
old,
effective,
BindingPriority.LocalValue));
SelectedIndex = index;
if (effective != null)
{
if (SelectedItems.Count != 1 || SelectedItems[0] != effective)
{
_syncingSelectedItems = true;
SelectedItems.Clear();
SelectedItems.Add(effective);
_syncingSelectedItems = false;
}
}
else if (SelectedItems.Count > 0)
{
SelectedItems.Clear();
}
}
}, value);
UpdateSelectedItem(IndexOf(Items, value));
}
else
{
@ -354,31 +307,23 @@ namespace Avalonia.Controls.Primitives
{
SelectedIndex = 0;
}
else
{
_selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count);
UpdateSelectedItem(_selection.First(), false);
}
break;
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Replace:
var selectedIndex = SelectedIndex;
if (selectedIndex >= e.OldStartingIndex &&
selectedIndex < e.OldStartingIndex + e.OldItems.Count)
{
if (!AlwaysSelected)
{
selectedIndex = SelectedIndex = -1;
}
else
{
LostSelection();
}
}
_selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count);
UpdateSelectedItem(_selection.First(), false);
ResetSelectedItems();
break;
var items = Items?.Cast<object>();
if (selectedIndex >= items.Count())
{
selectedIndex = SelectedIndex = items.Count() - 1;
}
case NotifyCollectionChangedAction.Replace:
UpdateSelectedItem(SelectedIndex, false);
ResetSelectedItems();
break;
case NotifyCollectionChangedAction.Move:
@ -439,11 +384,7 @@ namespace Avalonia.Controls.Primitives
{
if (i.ContainerControl != null && i.Item != null)
{
var ms = MemberSelector;
bool selected = ms == null ?
SelectedItems.Contains(i.Item) :
SelectedItems.OfType<object>().Any(v => Equals(ms.Select(v), i.Item));
bool selected = _selection.Contains(i.Index);
MarkContainerSelected(i.ContainerControl, selected);
}
}
@ -476,9 +417,12 @@ namespace Avalonia.Controls.Primitives
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
if (ItemCount > 0 &&
Match(keymap.SelectAll) &&
(((SelectionMode & SelectionMode.Multiple) != 0) ||
(SelectionMode & SelectionMode.Toggle) != 0))
{
SynchronizeItems(SelectedItems, Items?.Cast<object>());
SelectAll();
e.Handled = true;
}
}
@ -520,6 +464,41 @@ namespace Avalonia.Controls.Primitives
return false;
}
/// <summary>
/// Selects all items in the control.
/// </summary>
protected void SelectAll()
{
if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0)
{
throw new NotSupportedException("Multiple selection is not enabled on this control.");
}
UpdateSelectedItems(() =>
{
_selection.Clear();
for (var i = 0; i < ItemCount; ++i)
{
_selection.Add(i);
}
UpdateSelectedItem(0, false);
foreach (var container in ItemContainerGenerator.Containers)
{
MarkItemSelected(container.Index, true);
}
ResetSelectedItems();
});
}
/// <summary>
/// Deselects all items in the control.
/// </summary>
protected void UnselectAll() => UpdateSelectedItem(-1);
/// <summary>
/// Updates the selection for an item based on user interaction.
/// </summary>
@ -527,51 +506,83 @@ namespace Avalonia.Controls.Primitives
/// <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="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 UpdateSelection(
int index,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
bool toggleModifier = false,
bool rightButton = false)
{
if (index != -1)
{
if (select)
{
var mode = SelectionMode;
var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0;
var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && SelectedIndex != -1 && rangeModifier;
var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
var range = multi && rangeModifier;
if (!toggle && !range)
if (range)
{
SelectedIndex = index;
}
else if (multi && range)
{
SynchronizeItems(
SelectedItems,
GetRange(Items, SelectedIndex, index));
UpdateSelectedItems(() =>
{
var start = SelectedIndex != -1 ? SelectedIndex : 0;
var step = start < index ? 1 : -1;
_selection.Clear();
for (var i = start; i != index; i += step)
{
_selection.Add(i);
}
_selection.Add(index);
var first = Math.Min(start, index);
var last = Math.Max(start, index);
foreach (var container in ItemContainerGenerator.Containers)
{
MarkItemSelected(
container.Index,
container.Index >= first && container.Index <= last);
}
ResetSelectedItems();
});
}
else
else if (multi && toggle)
{
var item = ElementAt(Items, index);
var i = SelectedItems.IndexOf(item);
if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1))
{
SelectedItems.Remove(item);
}
else
UpdateSelectedItems(() =>
{
if (multi)
if (!_selection.Contains(index))
{
SelectedItems.Add(item);
_selection.Add(index);
MarkItemSelected(index, true);
SelectedItems.Add(ElementAt(Items, index));
}
else
{
SelectedIndex = index;
_selection.Remove(index);
MarkItemSelected(index, false);
if (index == _selectedIndex)
{
UpdateSelectedItem(_selection.First(), false);
}
SelectedItems.Remove(ElementAt(Items, index));
}
}
});
}
else if (toggle)
{
SelectedIndex = (SelectedIndex == index) ? -1 : index;
}
else
{
UpdateSelectedItem(index, !(rightButton && _selection.Contains(index)));
}
if (Presenter?.Panel != null)
@ -596,17 +607,19 @@ namespace Avalonia.Controls.Primitives
/// <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="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 UpdateSelection(
IControl container,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
bool toggleModifier = false,
bool rightButton = false)
{
var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1;
if (index != -1)
{
UpdateSelection(index, select, rangeModifier, toggleModifier);
UpdateSelection(index, select, rangeModifier, toggleModifier, rightButton);
}
}
@ -618,6 +631,7 @@ namespace Avalonia.Controls.Primitives
/// <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="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
/// <returns>
/// True if the event originated from a container that belongs to the control; otherwise
/// false.
@ -626,51 +640,20 @@ namespace Avalonia.Controls.Primitives
IInteractive eventSource,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
bool toggleModifier = false,
bool rightButton = false)
{
var container = GetContainerFromEventSource(eventSource);
if (container != null)
{
UpdateSelection(container, select, rangeModifier, toggleModifier);
UpdateSelection(container, select, rangeModifier, toggleModifier, rightButton);
return true;
}
return false;
}
/// <summary>
/// Makes a list of objects equal another.
/// </summary>
/// <param name="items">The items collection.</param>
/// <param name="desired">The desired items.</param>
internal static void SynchronizeItems(IList items, IEnumerable<object> desired)
{
var index = 0;
foreach (object item in desired)
{
int itemIndex = items.IndexOf(item);
if (itemIndex == -1)
{
items.Insert(index, item);
}
else if(itemIndex != index)
{
items.RemoveAt(itemIndex);
items.Insert(index, item);
}
++index;
}
while (index < items.Count)
{
items.RemoveAt(items.Count - 1);
}
}
/// <summary>
/// Gets a range of items from an IEnumerable.
/// </summary>
@ -678,17 +661,19 @@ namespace Avalonia.Controls.Primitives
/// <param name="first">The index of the first item.</param>
/// <param name="last">The index of the last item.</param>
/// <returns>The items.</returns>
private static IEnumerable<object> GetRange(IEnumerable items, int first, int last)
private static List<object> GetRange(IEnumerable items, int first, int last)
{
var list = (items as IList) ?? items.Cast<object>().ToList();
int step = first > last ? -1 : 1;
var step = first > last ? -1 : 1;
var result = new List<object>();
for (int i = first; i != last; i += step)
{
yield return list[i];
result.Add(list[i]);
}
yield return list[last];
result.Add(list[last]);
return result;
}
/// <summary>
@ -724,19 +709,14 @@ namespace Avalonia.Controls.Primitives
private void LostSelection()
{
var items = Items?.Cast<object>();
var index = -1;
if (items != null && AlwaysSelected)
{
var index = Math.Min(SelectedIndex, items.Count() - 1);
if (index > -1)
{
SelectedItem = items.ElementAt(index);
return;
}
index = Math.Min(SelectedIndex, items.Count() - 1);
}
SelectedIndex = -1;
SelectedIndex = index;
}
/// <summary>
@ -793,7 +773,7 @@ namespace Avalonia.Controls.Primitives
/// </summary>
/// <param name="item">The item.</param>
/// <param name="selected">Whether the item should be selected or deselected.</param>
private void MarkItemSelected(object item, bool selected)
private int MarkItemSelected(object item, bool selected)
{
var index = IndexOf(Items, item);
@ -801,6 +781,21 @@ namespace Avalonia.Controls.Primitives
{
MarkItemSelected(index, selected);
}
return index;
}
private void ResetSelectedItems()
{
UpdateSelectedItems(() =>
{
SelectedItems.Clear();
foreach (var i in _selection)
{
SelectedItems.Add(ElementAt(Items, i));
}
});
}
/// <summary>
@ -810,95 +805,97 @@ namespace Avalonia.Controls.Primitives
/// <param name="e">The event args.</param>
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var generator = ItemContainerGenerator;
if (_syncingSelectedItems)
{
return;
}
void Add(IList newItems, IList addedItems = null)
{
foreach (var item in newItems)
{
var index = MarkItemSelected(item, true);
if (index != -1 && _selection.Add(index) && addedItems != null)
{
addedItems.Add(item);
}
}
}
void UpdateSelection()
{
if ((SelectedIndex != -1 && !_selection.Contains(SelectedIndex)) ||
(SelectedIndex == -1 && _selection.HasItems))
{
_selectedIndex = _selection.First();
_selectedItem = ElementAt(Items, _selectedIndex);
RaisePropertyChanged(SelectedIndexProperty, -1, _selectedIndex, BindingPriority.LocalValue);
RaisePropertyChanged(SelectedItemProperty, null, _selectedItem, BindingPriority.LocalValue);
if (AutoScrollToSelectedItem)
{
ScrollIntoView(_selectedIndex);
}
}
}
IList added = null;
IList removed = null;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
SelectedItemsAdded(e.NewItems.Cast<object>().ToList());
if (AutoScrollToSelectedItem)
{
ScrollIntoView(e.NewItems[0]);
Add(e.NewItems);
UpdateSelection();
added = e.NewItems;
}
added = e.NewItems;
break;
case NotifyCollectionChangedAction.Remove:
if (SelectedItems.Count == 0)
{
if (!_syncingSelectedItems)
{
SelectedIndex = -1;
}
SelectedIndex = -1;
}
foreach (var item in e.OldItems)
{
MarkItemSelected(item, false);
var index = MarkItemSelected(item, false);
_selection.Remove(index);
}
removed = e.OldItems;
break;
case NotifyCollectionChangedAction.Replace:
throw new NotSupportedException("Replacing items in a SelectedItems collection is not supported.");
case NotifyCollectionChangedAction.Move:
throw new NotSupportedException("Moving items in a SelectedItems collection is not supported.");
case NotifyCollectionChangedAction.Reset:
if (generator != null)
{
removed = new List<object>();
added = new List<object>();
foreach (var item in generator.Containers)
foreach (var index in _selection.ToList())
{
if (item?.ContainerControl != null)
var item = ElementAt(Items, index);
if (!SelectedItems.Contains(item))
{
if (MarkContainerSelected(item.ContainerControl, false))
{
removed.Add(item.Item);
}
MarkItemSelected(index, false);
removed.Add(item);
_selection.Remove(index);
}
}
}
if (SelectedItems.Count > 0)
{
_selectedItem = null;
SelectedItemsAdded(SelectedItems);
added = SelectedItems;
}
else if (!_syncingSelectedItems)
{
SelectedIndex = -1;
}
break;
case NotifyCollectionChangedAction.Replace:
foreach (var item in e.OldItems)
{
MarkItemSelected(item, false);
}
foreach (var item in e.NewItems)
{
MarkItemSelected(item, true);
Add(SelectedItems, added);
UpdateSelection();
}
if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems)
{
var oldItem = SelectedItem;
var oldIndex = SelectedIndex;
var item = SelectedItems[0];
var index = IndexOf(Items, item);
_selectedIndex = index;
_selectedItem = item;
RaisePropertyChanged(SelectedIndexProperty, oldIndex, index, BindingPriority.LocalValue);
RaisePropertyChanged(SelectedItemProperty, oldItem, item, BindingPriority.LocalValue);
}
added = e.NewItems;
removed = e.OldItems;
break;
}
@ -912,34 +909,6 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Called when items are added to the <see cref="SelectedItems"/> collection.
/// </summary>
/// <param name="items">The added items.</param>
private void SelectedItemsAdded(IList items)
{
if (items.Count > 0)
{
foreach (var item in items)
{
MarkItemSelected(item, true);
}
if (SelectedItem == null && !_syncingSelectedItems)
{
var index = IndexOf(Items, items[0]);
if (index != -1)
{
_selectedItem = items[0];
_selectedIndex = index;
RaisePropertyChanged(SelectedIndexProperty, -1, index, BindingPriority.LocalValue);
RaisePropertyChanged(SelectedItemProperty, null, items[0], BindingPriority.LocalValue);
}
}
}
}
/// <summary>
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary>
@ -970,6 +939,112 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Updates the selection due to a change to <see cref="SelectedIndex"/> or
/// <see cref="SelectedItem"/>.
/// </summary>
/// <param name="index">The new selected index.</param>
/// <param name="clear">Whether to clear existing selection.</param>
private void UpdateSelectedItem(int index, bool clear = true)
{
var oldIndex = _selectedIndex;
var oldItem = _selectedItem;
if (index == -1 && AlwaysSelected)
{
index = Math.Min(SelectedIndex, ItemCount - 1);
}
var item = ElementAt(Items, index);
var added = -1;
HashSet<int> removed = null;
_selectedIndex = index;
_selectedItem = item;
if (oldIndex != index || _selection.HasMultiple)
{
if (clear)
{
removed = _selection.Clear();
}
if (index != -1)
{
if (_selection.Add(index))
{
added = index;
}
if (removed?.Contains(index) == true)
{
removed.Remove(index);
added = -1;
}
}
if (removed != null)
{
foreach (var i in removed)
{
MarkItemSelected(i, false);
}
}
MarkItemSelected(index, true);
RaisePropertyChanged(
SelectedIndexProperty,
oldIndex,
index);
}
if (!Equals(item, oldItem))
{
RaisePropertyChanged(
SelectedItemProperty,
oldItem,
item);
}
if (removed != null && index != -1)
{
removed.Remove(index);
}
if (added != -1 || removed?.Count > 0)
{
ResetSelectedItems();
var e = new SelectionChangedEventArgs(
SelectionChangedEvent,
added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty<object>(),
removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty<object>());
RaiseEvent(e);
}
}
private void UpdateSelectedItems(Action action)
{
try
{
_syncingSelectedItems = true;
action();
}
catch (Exception ex)
{
Logger.Error(
LogArea.Property,
this,
"Error thrown updating SelectedItems: {Error}",
ex);
}
finally
{
_syncingSelectedItems = false;
}
}
private void UpdateFinished()
{
if (_updateSelectedIndex != int.MinValue)
@ -981,5 +1056,104 @@ namespace Avalonia.Controls.Primitives
SelectedItems = _updateSelectedItems;
}
}
private class Selection : IEnumerable<int>
{
private readonly List<int> _list = new List<int>();
private HashSet<int> _set = new HashSet<int>();
public bool HasItems => _set.Count > 0;
public bool HasMultiple => _set.Count > 1;
public bool Add(int index)
{
if (index == -1)
{
throw new ArgumentException("Invalid index", "index");
}
if (_set.Add(index))
{
_list.Add(index);
return true;
}
return false;
}
public bool Remove(int index)
{
if (_set.Remove(index))
{
_list.RemoveAll(x => x == index);
return true;
}
return false;
}
public HashSet<int> Clear()
{
var result = _set;
_list.Clear();
_set = new HashSet<int>();
return result;
}
public void ItemsInserted(int index, int count)
{
_set = new HashSet<int>();
for (var i = 0; i < _list.Count; ++i)
{
var ix = _list[i];
if (ix >= index)
{
var newIndex = ix + count;
_list[i] = newIndex;
_set.Add(newIndex);
}
else
{
_set.Add(ix);
}
}
}
public void ItemsRemoved(int index, int count)
{
var last = (index + count) - 1;
_set = new HashSet<int>();
for (var i = 0; i < _list.Count; ++i)
{
var ix = _list[i];
if (ix >= index && ix <= last)
{
_list.RemoveAt(i--);
}
else if (ix > last)
{
var newIndex = ix - count;
_list[i] = newIndex;
_set.Add(newIndex);
}
else
{
_set.Add(ix);
}
}
}
public bool Contains(int index) => _set.Contains(index);
public int First() => HasItems ? _list[0] : -1;
public IEnumerator<int> GetEnumerator() => _set.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}

26
src/Avalonia.Controls/TreeView.cs

@ -409,7 +409,7 @@ namespace Avalonia.Controls
if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
{
SelectingItemsControl.SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
e.Handled = true;
}
}
@ -521,7 +521,7 @@ namespace Avalonia.Controls
}
else if (multi && range)
{
SelectingItemsControl.SynchronizeItems(
SynchronizeItems(
SelectedItems,
GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem));
}
@ -778,5 +778,27 @@ namespace Avalonia.Controls
container.Classes.Set(":selected", selected);
}
}
/// <summary>
/// Makes a list of objects equal another (though doesn't preserve order).
/// </summary>
/// <param name="items">The items collection.</param>
/// <param name="desired">The desired items.</param>
private static void SynchronizeItems(IList items, IEnumerable<object> desired)
{
var list = items.Cast<object>().ToList();
var toRemove = list.Except(desired).ToList();
var toAdd = desired.Except(list).ToList();
foreach (var i in toRemove)
{
items.Remove(i);
}
foreach (var i in toAdd)
{
items.Add(i);
}
}
}
}

115
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -536,6 +536,9 @@ namespace Avalonia.Controls.UnitTests.Primitives
SelectedIndex = 1,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var called = false;
target.SelectionChanged += (s, e) =>
@ -545,8 +548,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
called = true;
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectedIndex = -1;
Assert.True(called);
@ -783,6 +784,116 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(2, vm.Child.SelectedIndex);
}
[Fact]
public void Should_Select_Correct_Item_When_Duplicate_Items_Are_Present()
{
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz"},
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Down((Interactive)target.Presenter.Panel.Children[3]);
Assert.Equal(3, target.SelectedIndex);
}
[Fact]
public void Should_Apply_Selected_Pseudoclass_To_Correct_Item_When_Duplicate_Items_Are_Present()
{
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Down((Interactive)target.Presenter.Panel.Children[3]);
Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[3].Classes);
}
[Fact]
public void Adding_Item_Before_SelectedItem_Should_Update_SelectedIndex()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
SelectedIndex = 1,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
items.Insert(0, "Qux");
Assert.Equal(2, target.SelectedIndex);
Assert.Equal("Bar", target.SelectedItem);
}
[Fact]
public void Removing_Item_Before_SelectedItem_Should_Update_SelectedIndex()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
SelectedIndex = 1,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
items.RemoveAt(0);
Assert.Equal(0, target.SelectedIndex);
Assert.Equal("Bar", target.SelectedItem);
}
[Fact]
public void Replacing_Selected_Item_Should_Update_SelectedItem()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
SelectedIndex = 1,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
items[1] = "Qux";
Assert.Equal(1, target.SelectedIndex);
Assert.Equal("Qux", target.SelectedItem);
}
private FuncControlTemplate Template()
{
return new FuncControlTemplate<SelectingItemsControl>(control =>

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

@ -4,12 +4,15 @@
using System;
using System.Collections;
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.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Data;
using Xunit;
@ -17,6 +20,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
public class SelectingItemsControlTests_Multiple
{
private MouseTestHelper _helper = new MouseTestHelper();
[Fact]
public void Setting_SelectedIndex_Should_Add_To_SelectedItems()
{
@ -258,31 +263,25 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
[Fact]
public void Replacing_First_SelectedItem_Should_Update_SelectedItem_SelectedIndex()
public void Setting_SelectedIndex_Should_Unmark_Previously_Selected_Containers()
{
var items = new[]
{
new ListBoxItem(),
new ListBoxItem(),
new ListBoxItem(),
};
var target = new TestSelector
{
Items = items,
Items = new[] { "foo", "bar", "baz" },
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectedIndex = 1;
target.SelectedItems[0] = items[2];
Assert.Equal(2, target.SelectedIndex);
Assert.Equal(items[2], target.SelectedItem);
Assert.False(items[0].IsSelected);
Assert.False(items[1].IsSelected);
Assert.True(items[2].IsSelected);
target.SelectedItems.Add("foo");
target.SelectedItems.Add("bar");
Assert.Equal(new[] { 0, 1 }, SelectedContainers(target));
target.SelectedIndex = 2;
Assert.Equal(new[] { 2 }, SelectedContainers(target));
}
[Fact]
@ -361,6 +360,52 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(new[] { "baz", "qux", "qiz" }, target.SelectedItems.Cast<object>().ToList());
}
[Fact]
public void Setting_SelectedIndex_After_Range_Should_Unmark_Previously_Selected_Containers()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar", "baz", "qux" },
Template = Template(),
SelectedIndex = 0,
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectRange(2);
Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target));
target.SelectedIndex = 3;
Assert.Equal(new[] { 3 }, SelectedContainers(target));
}
[Fact]
public void Toggling_Selection_After_Range_Should_Work()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar", "baz", "foo", "bar", "baz" },
Template = Template(),
SelectedIndex = 0,
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectRange(3);
Assert.Equal(new[] { 0, 1, 2, 3 }, SelectedContainers(target));
target.Toggle(4);
Assert.Equal(new[] { 0, 1, 2, 3, 4 }, SelectedContainers(target));
}
[Fact]
public void Suprious_SelectedIndex_Changes_Should_Not_Be_Triggered()
{
@ -382,6 +427,40 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(new[] { -1, 1, 0 }, selectedIndexes);
}
[Fact]
public void Can_Set_SelectedIndex_To_Another_Selected_Item()
{
var target = new TestSelector
{
Items = new[] { "foo", "bar", "baz" },
Template = Template(),
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectedItems.Add("foo");
target.SelectedItems.Add("bar");
Assert.Equal(0, target.SelectedIndex);
Assert.Equal(new[] { "foo", "bar" }, target.SelectedItems);
Assert.Equal(new[] { 0, 1 }, SelectedContainers(target));
var raised = false;
target.SelectionChanged += (s, e) =>
{
raised = true;
Assert.Empty(e.AddedItems);
Assert.Equal(new[] { "foo" }, e.RemovedItems);
};
target.SelectedIndex = 1;
Assert.True(raised);
Assert.Equal(1, target.SelectedIndex);
Assert.Equal(new[] { "bar" }, target.SelectedItems);
Assert.Equal(new[] { 1 }, SelectedContainers(target));
}
/// <summary>
/// Tests a problem discovered with ListBox with selection.
/// </summary>
@ -471,6 +550,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
DataContext = items,
Template = Template(),
Items = items,
};
var called = false;
@ -540,35 +620,418 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.True(called);
}
[Fact]
public void Shift_Selecting_From_No_Selection_Selects_From_Start()
{
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Shift);
var panel = target.Presenter.Panel;
Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems);
Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target));
}
[Fact]
public void Replacing_SelectedItems_Should_Raise_SelectionChanged_With_CorrectItems()
public void Ctrl_Selecting_SelectedItem_With_Multiple_Selection_Active_Sets_SelectedItem_To_Next_Selection()
{
var items = new[] { "foo", "bar", "baz" };
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz", "Qux" },
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Click((Interactive)target.Presenter.Panel.Children[1]);
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control);
_helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control);
Assert.Equal(1, target.SelectedIndex);
Assert.Equal("Bar", target.SelectedItem);
Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems);
_helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control);
Assert.Equal(2, target.SelectedIndex);
Assert.Equal("Baz", target.SelectedItem);
Assert.Equal(new[] { "Baz", "Qux" }, target.SelectedItems);
}
[Fact]
public void Ctrl_Selecting_Non_SelectedItem_With_Multiple_Selection_Active_Leaves_SelectedItem_The_Same()
{
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Click((Interactive)target.Presenter.Panel.Children[1]);
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control);
Assert.Equal(1, target.SelectedIndex);
Assert.Equal("Bar", target.SelectedItem);
_helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control);
Assert.Equal(1, target.SelectedIndex);
Assert.Equal("Bar", target.SelectedItem);
}
[Fact]
public void Should_Ctrl_Select_Correct_Item_When_Duplicate_Items_Are_Present()
{
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Click((Interactive)target.Presenter.Panel.Children[3]);
_helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control);
var panel = target.Presenter.Panel;
Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems);
Assert.Equal(new[] { 3, 4 }, SelectedContainers(target));
}
[Fact]
public void Should_Shift_Select_Correct_Item_When_Duplicates_Are_Present()
{
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Click((Interactive)target.Presenter.Panel.Children[3]);
_helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift);
var panel = target.Presenter.Panel;
Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems);
Assert.Equal(new[] { 3, 4, 5 }, SelectedContainers(target));
}
[Fact]
public void Can_Shift_Select_All_Items_When_Duplicates_Are_Present()
{
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Click((Interactive)target.Presenter.Panel.Children[0]);
_helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift);
var panel = target.Presenter.Panel;
Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems);
Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, SelectedContainers(target));
}
[Fact]
public void Duplicate_Items_Are_Added_To_SelectedItems_In_Order()
{
var target = new ListBox
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
_helper.Click((Interactive)target.Presenter.Panel.Children[0]);
Assert.Equal(new[] { "Foo" }, target.SelectedItems);
_helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control);
Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems);
_helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control);
Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems);
_helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control);
Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems);
}
[Fact]
public void SelectAll_Sets_SelectedIndex_And_SelectedItem()
{
var target = new TestSelector
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectAll();
Assert.Equal(0, target.SelectedIndex);
Assert.Equal("Foo", target.SelectedItem);
}
[Fact]
public void UnselectAll_Clears_SelectedIndex_And_SelectedItem()
{
var target = new TestSelector
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
SelectedIndex = 0,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.UnselectAll();
Assert.Equal(-1, target.SelectedIndex);
Assert.Equal(null, target.SelectedItem);
}
[Fact]
public void SelectAll_Handles_Duplicate_Items()
{
var target = new TestSelector
{
Template = Template(),
Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectAll();
Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems);
}
[Fact]
public void Adding_Item_Before_SelectedItems_Should_Update_Selection()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectAll();
items.Insert(0, "Qux");
Assert.Equal(1, target.SelectedIndex);
Assert.Equal("Foo", target.SelectedItem);
Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems);
Assert.Equal(new[] { 1, 2, 3 }, SelectedContainers(target));
}
[Fact]
public void Removing_Item_Before_SelectedItem_Should_Update_Selection()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new TestSelector
{
Template = Template(),
SelectedItem = "bar",
Items = items,
SelectionMode = SelectionMode.Multiple,
};
var called = false;
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectionChanged += (s, e) =>
target.SelectedIndex = 1;
target.SelectRange(2);
Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems);
items.RemoveAt(0);
Assert.Equal(0, target.SelectedIndex);
Assert.Equal("Bar", target.SelectedItem);
Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems);
Assert.Equal(new[] { 0, 1 }, SelectedContainers(target));
}
[Fact]
public void Removing_SelectedItem_With_Multiple_Selection_Active_Should_Update_Selection()
{
var items = new ObservableCollection<string>
{
Assert.Equal(new[] { "foo",}, e.AddedItems.Cast<object>());
Assert.Equal(new[] { "bar" }, e.RemovedItems.Cast<object>());
called = true;
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectedItems[0] = "foo";
Assert.True(called);
target.SelectAll();
items.RemoveAt(0);
Assert.Equal(0, target.SelectedIndex);
Assert.Equal("Bar", target.SelectedItem);
Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems);
Assert.Equal(new[] { 0, 1 }, SelectedContainers(target));
}
[Fact]
public void Replacing_Selected_Item_Should_Update_SelectedItems()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"Baz"
};
var target = new ListBox
{
Template = Template(),
Items = items,
SelectionMode = SelectionMode.Multiple,
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
target.SelectAll();
items[1] = "Qux";
Assert.Equal(new[] { "Foo", "Qux", "Baz" }, target.SelectedItems);
}
[Fact]
public void Left_Click_On_SelectedItem_Should_Clear_Existing_Selection()
{
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();
target.SelectAll();
Assert.Equal(3, target.SelectedItems.Count);
_helper.Click((Interactive)target.Presenter.Panel.Children[0]);
Assert.Equal(1, target.SelectedItems.Count);
Assert.Equal(new[] { "Foo", }, target.SelectedItems);
Assert.Equal(new[] { 0 }, SelectedContainers(target));
}
[Fact]
public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection()
{
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();
target.SelectAll();
Assert.Equal(3, target.SelectedItems.Count);
_helper.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right);
Assert.Equal(3, target.SelectedItems.Count);
}
[Fact]
public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection()
{
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[1], modifiers: InputModifiers.Shift);
Assert.Equal(2, target.SelectedItems.Count);
_helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right);
Assert.Equal(1, target.SelectedItems.Count);
}
private IEnumerable<int> SelectedContainers(SelectingItemsControl target)
{
return target.Presenter.Panel.Children
.Select((x, i) => x.Classes.Contains(":selected") ? i : -1)
.Where(x => x != -1);
}
private FuncControlTemplate Template()
{
@ -598,10 +1061,10 @@ namespace Avalonia.Controls.UnitTests.Primitives
set { base.SelectionMode = value; }
}
public void SelectRange(int index)
{
UpdateSelection(index, true, true);
}
public new void SelectAll() => base.SelectAll();
public new void UnselectAll() => base.UnselectAll();
public void SelectRange(int index) => UpdateSelection(index, true, true);
public void Toggle(int index) => UpdateSelection(index, true, false, true);
}
private class OldDataContextViewModel

Loading…
Cancel
Save