Browse Source

Implemented SelectionMode.AlwaysSelected.

Steven Kirk 5 years ago
parent
commit
1120820b7e

+ 19 - 8
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -634,6 +634,20 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
+        /// <summary>
+        /// Called when <see cref="ISelectionModel.LostSelection"/> event is raised on
+        /// <see cref="Selection"/>.
+        /// </summary>
+        /// <param name="sender">The sender.</param>
+        /// <param name="e">The event args.</param>
+        private void OnSelectionModelLostSelection(object sender, EventArgs e)
+        {
+            if (AlwaysSelected)
+            {
+                SelectedIndex = 0;
+            }
+        }
+
         /// <summary>
         /// Called when a container raises the <see cref="IsSelectedChangedEvent"/>.
         /// </summary>
@@ -764,6 +778,7 @@ namespace Avalonia.Controls.Primitives
 
             model.PropertyChanged += OnSelectionModelPropertyChanged;
             model.SelectionChanged += OnSelectionModelSelectionChanged;
+            model.LostSelection += OnSelectionModelLostSelection;
 
             if (model.SingleSelect)
             {
@@ -777,14 +792,10 @@ namespace Avalonia.Controls.Primitives
             _oldSelectedIndex = model.SelectedIndex;
             _oldSelectedItem = model.SelectedItem;
 
-            //if (model.AutoSelect)
-            //{
-            //    SelectionMode |= SelectionMode.AlwaysSelected;
-            //}
-            //else
-            //{
-            //    SelectionMode &= ~SelectionMode.AlwaysSelected;
-            //}
+            if (AlwaysSelected && model.Count == 0)
+            {
+                model.SelectedIndex = 0;
+            }
 
             //if (Items is INotifyCollectionChanged incc)
             //{

+ 23 - 6
src/Avalonia.Controls/Selection/SelectionModel.cs

@@ -320,12 +320,18 @@ namespace Avalonia.Controls.Selection
 
         private protected override void OnSelectionChanged(IReadOnlyList<T> deselectedItems)
         {
-            if (SelectionChanged is object || _untypedSelectionChanged is object)
+            // Note: We're *not* putting this in a using scope. A collection update is still in progress
+            // so the operation won't get commited by normal means: we have to commit it manually.
+            var update = BatchUpdate();
+
+            update.Operation.DeselectedItems = deselectedItems;
+
+            if (_selectedIndex == -1 && LostSelection is object)
             {
-                var e = new SelectionModelSelectionChangedEventArgs<T>(deselectedItems: deselectedItems);
-                SelectionChanged?.Invoke(this, e);
-                _untypedSelectionChanged?.Invoke(this, e);
+                LostSelection(this, EventArgs.Empty);
             }
+
+            CommitOperation(update.Operation);
         }
 
         private protected override CollectionChangeState OnItemsAdded(int index, IList items)
@@ -613,13 +619,23 @@ namespace Avalonia.Controls.Selection
                         }
                     }
 
-                    if (deselected?.Count > 0 || selected?.Count > 0)
+                    if (deselected?.Count > 0 || selected?.Count > 0 || operation.DeselectedItems is object)
                     {
+                        // If the operation was caused by Source being updated, then use a null source
+                        // so that the items will appear as nulls.
                         var deselectedSource = operation.IsSourceUpdate ? null : ItemsView;
+
+                        // If the operation contains DeselectedItems then we're notifying a source
+                        // CollectionChanged event. LostFocus may have caused another item to have been
+                        // selected, but it can't have caused a deselection (as it was called due to
+                        // selection being lost) so we're ok to discard `deselected` here.
+                        var deselectedItems = operation.DeselectedItems ??
+                            SelectedItems<T>.Create(deselected, deselectedSource);
+
                         var e = new SelectionModelSelectionChangedEventArgs<T>(
                             SelectedIndexes<T>.Create(deselected),
                             SelectedIndexes<T>.Create(selected),
-                            SelectedItems<T>.Create(deselected, deselectedSource),
+                            deselectedItems,
                             SelectedItems<T>.Create(selected, ItemsView));
                         SelectionChanged?.Invoke(this, e);
                         _untypedSelectionChanged?.Invoke(this, e);
@@ -689,6 +705,7 @@ namespace Avalonia.Controls.Selection
             public int SelectedIndex { get; set; }
             public List<IndexRange>? SelectedRanges { get; set; }
             public List<IndexRange>? DeselectedRanges { get; set; }
+            public IReadOnlyList<T> DeselectedItems { get; set; }
         }
     }
 }

+ 2 - 2
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs

@@ -75,8 +75,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.SelectedIndex = 2;
             items.RemoveAt(2);
 
-            Assert.Equal(2, target.SelectedIndex);
-            Assert.Equal("qux", target.SelectedItem);
+            Assert.Equal(0, target.SelectedIndex);
+            Assert.Equal("foo", target.SelectedItem);
         }
 
         [Fact]

+ 31 - 1
tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs

@@ -1394,7 +1394,7 @@ namespace Avalonia.Controls.UnitTests.Selection
         public class LostSelection
         {
             [Fact]
-            public void Can_Select_First_Item_On_LostSelection()
+            public void LostSelection_Called_On_Clear()
             {
                 var target = CreateTarget();
                 var raised = 0;
@@ -1417,6 +1417,36 @@ namespace Avalonia.Controls.UnitTests.Selection
 
                 target.Clear();
 
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void LostSelection_Called_When_Selection_Removed()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var raised = 0;
+
+                target.SelectRange(1, 3);
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar", "baz", "qux" }, e.DeselectedItems);
+                    Assert.Equal(new[] { 0 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "quux" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.LostSelection += (s, e) =>
+                {
+                    target.Select(0);
+                };
+
+                data.RemoveRange(0, 4);
+
+                Assert.Equal(0, target.SelectedIndex);
                 Assert.Equal(1, raised);
             }
         }

+ 31 - 1
tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs

@@ -1066,7 +1066,7 @@ namespace Avalonia.Controls.UnitTests.Selection
         public class LostSelection
         {
             [Fact]
-            public void Can_Select_First_Item_On_LostSelection()
+            public void LostSelection_Called_On_Clear()
             {
                 var target = CreateTarget();
                 var raised = 0;
@@ -1089,6 +1089,36 @@ namespace Avalonia.Controls.UnitTests.Selection
 
                 target.Clear();
 
+                Assert.Equal(0, target.SelectedIndex);
+                Assert.Equal(1, raised);
+            }
+
+            [Fact]
+            public void LostSelection_Called_When_SelectedItem_Removed()
+            {
+                var target = CreateTarget();
+                var data = (AvaloniaList<string>)target.Source!;
+                var raised = 0;
+
+                target.SelectedIndex = 1;
+
+                target.SelectionChanged += (s, e) =>
+                {
+                    Assert.Empty(e.DeselectedIndexes);
+                    Assert.Equal(new[] { "bar" }, e.DeselectedItems);
+                    Assert.Equal(new[] { 0 }, e.SelectedIndexes);
+                    Assert.Equal(new[] { "foo" }, e.SelectedItems);
+                    ++raised;
+                };
+
+                target.LostSelection += (s, e) =>
+                {
+                    target.Select(0);
+                };
+
+                data.RemoveAt(1);
+
+                Assert.Equal(0, target.SelectedIndex);
                 Assert.Equal(1, raised);
             }
         }