From 2e9fbe238ba5c2c9dd82a3f1ff05a6a329787e02 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 11:48:08 +0200 Subject: [PATCH 01/13] Refactor selection in SelectingItemsControl. Instead of using `SelectedItems` as the canonical source of selection information, use an internal `Selection` class which keeps track of the selection in terms of item indexes. Fixes #2004 --- .../Primitives/SelectingItemsControl.cs | 564 ++++++++++++------ .../Primitives/SelectingItemsControlTests.cs | 41 +- .../SelectingItemsControlTests_Multiple.cs | 302 +++++++++- 3 files changed, 677 insertions(+), 230 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7b91d6235d..143b2ffd7d 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/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(); + private 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 notifyWrapper) => - { - var old = backing; - var effective = (val >= 0 && val < Items?.Cast().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 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 { @@ -439,11 +392,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().Any(v => Equals(ms.Select(v), i.Item)); - + bool selected = _selection.Contains(i.Index); MarkContainerSelected(i.ContainerControl, selected); } } @@ -476,9 +425,12 @@ namespace Avalonia.Controls.Primitives var keymap = AvaloniaLocator.Current.GetService(); bool Match(List 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()); + SelectAll(); e.Handled = true; } } @@ -520,6 +472,52 @@ namespace Avalonia.Controls.Primitives return false; } + /// + /// Selects all items in the control. + /// + 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); + } + + foreach (var container in ItemContainerGenerator.Containers) + { + MarkItemSelected(container.Index, true); + } + + ResetSelectedItems(GetRange(Items, 0, ItemCount - 1)); + }); + } + + /// + /// Deselects all items in the control. + /// + protected void UnselectAll() + { + UpdateSelectedItems(() => + { + _selection.Clear(); + + foreach (var container in ItemContainerGenerator.Containers) + { + MarkItemSelected(container.Index, false); + } + + SelectedItems.Clear(); + }); + } + /// /// Updates the selection for an item based on user interaction. /// @@ -538,40 +536,66 @@ namespace Avalonia.Controls.Primitives 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) - { - SelectedIndex = index; - } - else if (multi && range) + if (range) { - SynchronizeItems( - SelectedItems, - GetRange(Items, SelectedIndex, index)); + UpdateSelectedItems(() => + { + var start = SelectedIndex != -1 ? SelectedIndex : 0; + var first = Math.Min(start, index); + var last = Math.Max(start, index); + + for (var i = first; i < last; ++i) + { + _selection.Add(i); + } + + _selection.Add(last); + + foreach (var container in ItemContainerGenerator.Containers) + { + MarkItemSelected( + container.Index, + container.Index >= first && container.Index <= last); + } + + ResetSelectedItems(GetRange(Items, start, index)); + }); } - else + else if (multi && toggle) { - var item = ElementAt(Items, index); - var i = SelectedItems.IndexOf(item); - - if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1)) + UpdateSelectedItems(() => { - SelectedItems.Remove(item); - } - else - { - 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 + { + SelectedIndex = index; } if (Presenter?.Panel != null) @@ -640,34 +664,24 @@ namespace Avalonia.Controls.Primitives } /// - /// Makes a list of objects equal another. + /// Makes a list of objects equal another (though doesn't preserve order). /// /// The items collection. /// The desired items. internal static void SynchronizeItems(IList items, IEnumerable desired) { - var index = 0; + var list = items.Cast().ToList(); + var toRemove = list.Except(desired).ToList(); + var toAdd = desired.Except(list).ToList(); - foreach (object item in desired) + foreach(var i in toRemove) { - int itemIndex = items.IndexOf(item); - - if (itemIndex == -1) - { - items.Insert(index, item); - } - else if(itemIndex != index) - { - items.RemoveAt(itemIndex); - items.Insert(index, item); - } - - ++index; + items.Remove(i); } - while (index < items.Count) + foreach (var i in toAdd) { - items.RemoveAt(items.Count - 1); + items.Add(i); } } @@ -678,17 +692,19 @@ namespace Avalonia.Controls.Primitives /// The index of the first item. /// The index of the last item. /// The items. - private static IEnumerable GetRange(IEnumerable items, int first, int last) + private static List GetRange(IEnumerable items, int first, int last) { var list = (items as IList) ?? items.Cast().ToList(); - int step = first > last ? -1 : 1; + var step = first > last ? -1 : 1; + var result = new List(); 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; } /// @@ -724,19 +740,14 @@ namespace Avalonia.Controls.Primitives private void LostSelection() { var items = Items?.Cast(); + 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; } /// @@ -793,7 +804,7 @@ namespace Avalonia.Controls.Primitives /// /// The item. /// Whether the item should be selected or deselected. - private void MarkItemSelected(object item, bool selected) + private int MarkItemSelected(object item, bool selected) { var index = IndexOf(Items, item); @@ -801,6 +812,18 @@ namespace Avalonia.Controls.Primitives { MarkItemSelected(index, selected); } + + return index; + } + + private void ResetSelectedItems(IEnumerable items) + { + SelectedItems.Clear(); + + foreach (var i in items) + { + SelectedItems.Add(i); + } } /// @@ -810,95 +833,97 @@ namespace Avalonia.Controls.Primitives /// The event args. 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().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(); + added = new List(); - 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); - } - 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); + Add(SelectedItems, added); + UpdateSelection(); } - added = e.NewItems; - removed = e.OldItems; break; } @@ -912,34 +937,6 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Called when items are added to the collection. - /// - /// The added items. - 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); - } - } - } - } - /// /// Subscribes to the CollectionChanged event, if any. /// @@ -970,6 +967,130 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Updates the selection due to a change to or + /// . + /// + /// The new selected index. + /// Whether to clear existing selection. + 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 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); + + UpdateSelectedItems(() => + { + if (clear) + { + SelectedItems.Clear(); + + if (index != -1) + { + SelectedItems.Add(item); + } + } + else + { + if (added != -1) + { + SelectedItems.Add(added); + } + } + }); + } + + if (removed != null && index != -1) + { + removed.Remove(index); + } + + if (added != -1 || removed?.Count > 0) + { + var e = new SelectionChangedEventArgs( + SelectionChangedEvent, + added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty(), + removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty()); + 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 +1102,56 @@ namespace Avalonia.Controls.Primitives SelectedItems = _updateSelectedItems; } } + + private class Selection : IEnumerable + { + private List _list = new List(); + private HashSet _set = new HashSet(); + + 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 Clear() + { + var result = _set; + _list.Clear(); + _set = new HashSet(); + return result; + } + + public bool Contains(int index) => _set.Contains(index); + + public int First() => HasItems ? _list[0] : -1; + + public IEnumerator GetEnumerator() => _set.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 8e421bf0a2..683b3b41e8 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/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,42 @@ 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]); + + var panel = target.Presenter.Panel; + + 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]); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[3].Classes); + } + private FuncControlTemplate Template() { return new FuncControlTemplate(control => diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 9ef7d567c8..e0a8fa81f4 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -10,6 +10,8 @@ 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 +19,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 +262,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 +359,52 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "baz", "qux", "qiz" }, target.SelectedItems.Cast().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 +426,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)); + } + /// /// Tests a problem discovered with ListBox with selection. /// @@ -471,6 +549,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { DataContext = items, Template = Template(), + Items = items, }; var called = false; @@ -540,35 +619,193 @@ 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.Down((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, + }; - var target = new TestSelector + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Down((Interactive)target.Presenter.Panel.Children[1]); + _helper.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); + _helper.Down((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.Down((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 { - Items = items, Template = Template(), - SelectedItem = "bar", + Items = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, }; - var called = false; + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + _helper.Down((Interactive)target.Presenter.Panel.Children[1]); + _helper.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); - target.SelectionChanged += (s, e) => + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + + _helper.Down((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 { - Assert.Equal(new[] { "foo",}, e.AddedItems.Cast()); - Assert.Equal(new[] { "bar" }, e.RemovedItems.Cast()); - called = true; + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedItems[0] = "foo"; + _helper.Down((Interactive)target.Presenter.Panel.Children[3]); + _helper.Down((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); - Assert.True(called); + 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.Down((Interactive)target.Presenter.Panel.Children[3]); + _helper.Down((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.Down((Interactive)target.Presenter.Panel.Children[0]); + _helper.Down((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.Down((Interactive)target.Presenter.Panel.Children[0]); + + Assert.Equal(new[] { "Foo" }, target.SelectedItems); + + _helper.Down((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); + + Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + + _helper.Down((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control); + + Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); + + _helper.Down((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); + + Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); + } + + [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); + } + + private IEnumerable 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 +835,11 @@ 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 void SelectRange(int index) => UpdateSelection(index, true, true); + + public void Toggle(int index) => UpdateSelection(index, true, false, true); } private class OldDataContextViewModel From ac59468ddc935830092cd7100299abb07fbe026e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 11:50:06 +0200 Subject: [PATCH 02/13] Add SelectAll and UnselectAll to ListBox. --- src/Avalonia.Controls/ListBox.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 041b81155a..3568183459 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -84,6 +84,16 @@ namespace Avalonia.Controls set { SetValue(VirtualizationModeProperty, value); } } + /// + /// Selects all items in the . + /// + public new void SelectAll() => base.SelectAll(); + + /// + /// Deselects all items in the . + /// + public new void UnselectAll() => base.UnselectAll(); + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { From f58f8950bd784a9d096eef03cf811f1f8b6cebd5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 11:56:55 +0200 Subject: [PATCH 03/13] Added failing test for #2574. --- .../Primitives/SelectingItemsControlTests.cs | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 683b3b41e8..3df66bb6cb 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -797,8 +797,6 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Presenter.ApplyTemplate(); _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - var panel = target.Presenter.Panel; - Assert.Equal(3, target.SelectedIndex); } @@ -815,11 +813,35 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Presenter.ApplyTemplate(); _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - var panel = target.Presenter.Panel; - Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[3].Classes); } + [Fact] + public void Adding_Item_Before_SelectedItem_Should_Update_SelectedIndex() + { + var items = new ObservableCollection + { + "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); + } + private FuncControlTemplate Template() { return new FuncControlTemplate(control => From 6926f605f541b298eed56cd877948b6aea1d91e7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 11:59:59 +0200 Subject: [PATCH 04/13] Update SelectedIndex when item added before selection. Fixes #2574. --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 143b2ffd7d..8cd714b1b8 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -307,6 +307,10 @@ namespace Avalonia.Controls.Primitives { SelectedIndex = 0; } + else if (e.NewStartingIndex <= SelectedIndex) + { + UpdateSelectedItem(SelectedIndex + e.NewItems.Count, false); + } break; From 1c85f409b3fa1e18cea40d714673f0dfdc00a0b3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 12:16:18 +0200 Subject: [PATCH 05/13] Set SelectedIndex/Item on (Un)SelectAll(). --- .../Primitives/SelectingItemsControl.cs | 17 ++------ .../SelectingItemsControlTests_Multiple.cs | 43 ++++++++++++++++++- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 8cd714b1b8..c1db8e80ff 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -495,6 +495,8 @@ namespace Avalonia.Controls.Primitives _selection.Add(i); } + UpdateSelectedItem(0, false); + foreach (var container in ItemContainerGenerator.Containers) { MarkItemSelected(container.Index, true); @@ -507,20 +509,7 @@ namespace Avalonia.Controls.Primitives /// /// Deselects all items in the control. /// - protected void UnselectAll() - { - UpdateSelectedItems(() => - { - _selection.Clear(); - - foreach (var container in ItemContainerGenerator.Containers) - { - MarkItemSelected(container.Index, false); - } - - SelectedItems.Clear(); - }); - } + protected void UnselectAll() => UpdateSelectedItem(-1); /// /// Updates the selection for an item based on user interaction. diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index e0a8fa81f4..13dd5724c2 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; @@ -783,6 +784,45 @@ namespace Avalonia.Controls.UnitTests.Primitives 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() { @@ -836,9 +876,8 @@ namespace Avalonia.Controls.UnitTests.Primitives } 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); } From ca23d03f98ecde3cab421215e9b0996a8693c673 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 12:27:38 +0200 Subject: [PATCH 06/13] Update SelectedIndex when items removed before selection. --- .../Primitives/SelectingItemsControl.cs | 14 ++++------ .../Primitives/SelectingItemsControlTests.cs | 26 +++++++++++++++++ .../SelectingItemsControlTests_Multiple.cs | 28 +++++++++++++++++++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index c1db8e80ff..080f09a4d3 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -316,26 +316,22 @@ namespace Avalonia.Controls.Primitives case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Replace: - var selectedIndex = SelectedIndex; - - if (selectedIndex >= e.OldStartingIndex && - selectedIndex < e.OldStartingIndex + e.OldItems.Count) + if (SelectedIndex >= e.OldStartingIndex && SelectedIndex < e.OldStartingIndex + e.OldItems.Count) { if (!AlwaysSelected) { - selectedIndex = SelectedIndex = -1; + SelectedIndex = -1; } else { LostSelection(); } } - - var items = Items?.Cast(); - if (selectedIndex >= items.Count()) + else if (e.OldStartingIndex <= SelectedIndex) { - selectedIndex = SelectedIndex = items.Count() - 1; + UpdateSelectedItem(SelectedIndex - e.OldItems.Count, false); } + break; case NotifyCollectionChangedAction.Move: diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 3df66bb6cb..4e7b7381cf 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -842,6 +842,32 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal("Bar", target.SelectedItem); } + [Fact] + public void Removing_Item_Before_SelectedItem_Should_Update_SelectedIndex() + { + var items = new ObservableCollection + { + "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); + } + private FuncControlTemplate Template() { return new FuncControlTemplate(control => diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 13dd5724c2..3639985140 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -840,6 +840,34 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); } + [Fact] + public void Adding_Item_Before_SelectedItems_Should_Update_Indexes() + { + var items = new ObservableCollection + { + "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); + } + + private IEnumerable SelectedContainers(SelectingItemsControl target) { return target.Presenter.Panel.Children From 94d71fb12febc7b95a04fb253a087365d14c4f50 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 13:40:10 +0200 Subject: [PATCH 07/13] More work on updating selection... ...when source items change. --- .../Primitives/SelectingItemsControl.cs | 107 +++++++++++++----- .../Primitives/SelectingItemsControlTests.cs | 26 +++++ .../SelectingItemsControlTests_Multiple.cs | 91 ++++++++++++++- 3 files changed, 193 insertions(+), 31 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 080f09a4d3..78de2112f6 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -307,31 +307,23 @@ namespace Avalonia.Controls.Primitives { SelectedIndex = 0; } - else if (e.NewStartingIndex <= SelectedIndex) + else { - UpdateSelectedItem(SelectedIndex + e.NewItems.Count, false); + _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); + UpdateSelectedItem(_selection.First(), false); } break; case NotifyCollectionChangedAction.Remove: - case NotifyCollectionChangedAction.Replace: - if (SelectedIndex >= e.OldStartingIndex && SelectedIndex < e.OldStartingIndex + e.OldItems.Count) - { - if (!AlwaysSelected) - { - SelectedIndex = -1; - } - else - { - LostSelection(); - } - } - else if (e.OldStartingIndex <= SelectedIndex) - { - UpdateSelectedItem(SelectedIndex - e.OldItems.Count, false); - } + _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); + UpdateSelectedItem(_selection.First(), false); + ResetSelectedItems(); + break; + case NotifyCollectionChangedAction.Replace: + UpdateSelectedItem(SelectedIndex, false); + ResetSelectedItems(); break; case NotifyCollectionChangedAction.Move: @@ -498,7 +490,7 @@ namespace Avalonia.Controls.Primitives MarkItemSelected(container.Index, true); } - ResetSelectedItems(GetRange(Items, 0, ItemCount - 1)); + ResetSelectedItems(); }); } @@ -534,15 +526,19 @@ namespace Avalonia.Controls.Primitives UpdateSelectedItems(() => { var start = SelectedIndex != -1 ? SelectedIndex : 0; - var first = Math.Min(start, index); - var last = Math.Max(start, index); + var step = start < index ? 1 : -1; - for (var i = first; i < last; ++i) + _selection.Clear(); + + for (var i = start; i != index; i += step) { _selection.Add(i); } - _selection.Add(last); + _selection.Add(index); + + var first = Math.Min(start, index); + var last = Math.Max(start, index); foreach (var container in ItemContainerGenerator.Containers) { @@ -551,7 +547,7 @@ namespace Avalonia.Controls.Primitives container.Index >= first && container.Index <= last); } - ResetSelectedItems(GetRange(Items, start, index)); + ResetSelectedItems(); }); } else if (multi && toggle) @@ -805,14 +801,17 @@ namespace Avalonia.Controls.Primitives return index; } - private void ResetSelectedItems(IEnumerable items) + private void ResetSelectedItems() { - SelectedItems.Clear(); - - foreach (var i in items) + UpdateSelectedItems(() => { - SelectedItems.Add(i); - } + SelectedItems.Clear(); + + foreach (var i in _selection) + { + SelectedItems.Add(ElementAt(Items, i)); + } + }); } /// @@ -1135,6 +1134,54 @@ namespace Avalonia.Controls.Primitives return result; } + public void ItemsInserted(int index, int count) + { + _set = new HashSet(); + + 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(); + + 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; diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 4e7b7381cf..75cababf54 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -868,6 +868,32 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal("Bar", target.SelectedItem); } + [Fact] + public void Replacing_Selected_Item_Should_Update_SelectedItem() + { + var items = new ObservableCollection + { + "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(control => diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 3639985140..d8de98f4f2 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -841,7 +841,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Adding_Item_Before_SelectedItems_Should_Update_Indexes() + public void Adding_Item_Before_SelectedItems_Should_Update_Selection() { var items = new ObservableCollection { @@ -865,8 +865,97 @@ namespace Avalonia.Controls.UnitTests.Primitives 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 + { + "Foo", + "Bar", + "Baz" + }; + + var target = new TestSelector + { + Template = Template(), + Items = items, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + 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 + { + "Foo", + "Bar", + "Baz" + }; + + var target = new ListBox + { + Template = Template(), + Items = items, + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + 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 + { + "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); + } private IEnumerable SelectedContainers(SelectingItemsControl target) { From 1066f57c9e34eb61f4ce69efd1c0933efd0930be Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Jun 2019 13:41:30 +0200 Subject: [PATCH 08/13] Move SynchronizeItems to TreeView. As it's no longer used in `SelectingItemsControl`. --- .../Primitives/SelectingItemsControl.cs | 22 ---------------- src/Avalonia.Controls/TreeView.cs | 26 +++++++++++++++++-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 78de2112f6..269f662a62 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -648,28 +648,6 @@ namespace Avalonia.Controls.Primitives return false; } - /// - /// Makes a list of objects equal another (though doesn't preserve order). - /// - /// The items collection. - /// The desired items. - internal static void SynchronizeItems(IList items, IEnumerable desired) - { - var list = items.Cast().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); - } - } - /// /// Gets a range of items from an IEnumerable. /// diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index c3fbce1d83..888f4a2013 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/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); } } + + /// + /// Makes a list of objects equal another (though doesn't preserve order). + /// + /// The items collection. + /// The desired items. + private static void SynchronizeItems(IList items, IEnumerable desired) + { + var list = items.Cast().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); + } + } } } From 8f318ef75be59ac5199f47e068de9b0f37939ef1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 16:13:08 +0200 Subject: [PATCH 09/13] Make some fields readonly. --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 269f662a62..a2f4793958 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -104,7 +104,7 @@ namespace Avalonia.Controls.Primitives private static readonly IList Empty = Array.Empty(); - private Selection _selection = new Selection(); + private readonly Selection _selection = new Selection(); private int _selectedIndex = -1; private object _selectedItem; private IList _selectedItems; @@ -1071,7 +1071,7 @@ namespace Avalonia.Controls.Primitives private class Selection : IEnumerable { - private List _list = new List(); + private readonly List _list = new List(); private HashSet _set = new HashSet(); public bool HasItems => _set.Count > 0; From bf767c2c7397f351f6e1d81b27375396f2fea356 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 16:47:51 +0200 Subject: [PATCH 10/13] Fix clearing selection when clicking on selected item. --- .../Primitives/SelectingItemsControl.cs | 22 ++--------------- .../SelectingItemsControlTests_Multiple.cs | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a2f4793958..ab07b3ffa9 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -999,26 +999,6 @@ namespace Avalonia.Controls.Primitives SelectedItemProperty, oldItem, item); - - UpdateSelectedItems(() => - { - if (clear) - { - SelectedItems.Clear(); - - if (index != -1) - { - SelectedItems.Add(item); - } - } - else - { - if (added != -1) - { - SelectedItems.Add(added); - } - } - }); } if (removed != null && index != -1) @@ -1028,6 +1008,8 @@ namespace Avalonia.Controls.Primitives if (added != -1 || removed?.Count > 0) { + ResetSelectedItems(); + var e = new SelectionChangedEventArgs( SelectionChangedEvent, added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty(), diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index d8de98f4f2..4167a8e490 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -957,6 +957,30 @@ namespace Avalonia.Controls.UnitTests.Primitives 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(x => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectAll(); + + Assert.Equal(3, target.SelectedItems.Count); + + _helper.Down((Interactive)target.Presenter.Panel.Children[0]); + + Assert.Equal(1, target.SelectedItems.Count); + Assert.Equal(new[] { "Foo", }, target.SelectedItems); + Assert.Equal(new[] { 0 }, SelectedContainers(target)); + } + private IEnumerable SelectedContainers(SelectingItemsControl target) { return target.Presenter.Panel.Children From f84f6068bc7fbd6e7d5d46a5cce1dcd6edbe86b8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 17:14:42 +0200 Subject: [PATCH 11/13] Added failing tests for #2660. --- .../SelectingItemsControlTests_Multiple.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 4167a8e490..c557ea88be 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -981,6 +981,51 @@ namespace Avalonia.Controls.UnitTests.Primitives 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(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(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 SelectedContainers(SelectingItemsControl target) { return target.Presenter.Panel.Children From 2b11ca12dd87b5d128a14bdb25cace0f1ff1252f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 17:15:53 +0200 Subject: [PATCH 12/13] Don't clear selection on right click. Fixes #2660. --- src/Avalonia.Controls/ListBox.cs | 3 ++- .../Primitives/SelectingItemsControl.cs | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 3568183459..3150b6be91 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -128,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); } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index ab07b3ffa9..91a9fa7e40 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -506,11 +506,13 @@ namespace Avalonia.Controls.Primitives /// 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 UpdateSelection( int index, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { if (index != -1) { @@ -580,7 +582,7 @@ namespace Avalonia.Controls.Primitives } else { - SelectedIndex = index; + UpdateSelectedItem(index, !(rightButton && _selection.Contains(index))); } if (Presenter?.Panel != null) @@ -605,17 +607,19 @@ namespace Avalonia.Controls.Primitives /// 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. 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); } } @@ -627,6 +631,7 @@ namespace Avalonia.Controls.Primitives /// 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. @@ -635,13 +640,14 @@ 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; } From ccf60266e8cfe73f98e249cffd3820209d8f0f38 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 17:16:27 +0200 Subject: [PATCH 13/13] Send clicks rather than mouse down messages. --- .../SelectingItemsControlTests_Multiple.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index c557ea88be..a33d97779e 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -633,7 +633,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Shift); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Shift); var panel = target.Presenter.Panel; @@ -653,15 +653,15 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[1]); - _helper.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); - _helper.Down((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control); + _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.Down((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); Assert.Equal(2, target.SelectedIndex); Assert.Equal("Baz", target.SelectedItem); @@ -680,13 +680,13 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[1]); - _helper.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); + _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.Down((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Control); Assert.Equal(1, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); @@ -704,8 +704,8 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - _helper.Down((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[3]); + _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); var panel = target.Presenter.Panel; @@ -725,8 +725,8 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[3]); - _helper.Down((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift); + _helper.Click((Interactive)target.Presenter.Panel.Children[3]); + _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift); var panel = target.Presenter.Panel; @@ -746,8 +746,8 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[0]); - _helper.Down((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: InputModifiers.Shift); var panel = target.Presenter.Panel; @@ -767,19 +767,19 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - _helper.Down((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); Assert.Equal(new[] { "Foo" }, target.SelectedItems); - _helper.Down((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); - _helper.Down((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control); Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); - _helper.Down((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); + _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); } @@ -974,7 +974,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(3, target.SelectedItems.Count); - _helper.Down((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); Assert.Equal(1, target.SelectedItems.Count); Assert.Equal(new[] { "Foo", }, target.SelectedItems);