소스 검색

Started refactoring ListBox selection.

- Don't make selection follow focus
- Move all keyboard interaction into `ListBox`
- Move all pointer interaction into `ListBoxItem`
- Added `ItemsControl.ItemsControlFromItemContaner` as in WPF
Steven Kirk 2 년 전
부모
커밋
9d16e33843

+ 24 - 0
src/Avalonia.Controls/ItemsControl.cs

@@ -365,6 +365,30 @@ namespace Avalonia.Controls
         /// </summary>
         public IEnumerable<Control> GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty<Control>();
 
+        /// <summary>
+        /// Returns the <see cref="ItemsControl"/> that owns the specified container control.
+        /// </summary>
+        /// <param name="container">The container.</param>
+        /// <returns>
+        /// The owning <see cref="ItemsControl"/> or null if the control is not an items container.
+        /// </returns>
+        public static ItemsControl? ItemsControlFromItemContaner(Control container)
+        {
+            var c = container.Parent as Control;
+
+            while (c is not null)
+            {
+                if (c is ItemsControl itemsControl)
+                {
+                    return itemsControl.IndexFromContainer(container) >= 0 ? itemsControl : null;
+                }
+
+                c = c.Parent as Control;
+            }
+
+            return null;
+        }
+
         /// <summary>
         /// Creates or a container that can be used to display an item.
         /// </summary>

+ 39 - 29
src/Avalonia.Controls/ListBox.cs

@@ -1,4 +1,8 @@
+using System;
 using System.Collections;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Selection;
@@ -47,7 +51,7 @@ namespace Avalonia.Controls
         /// </summary>
         [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1010",
             Justification = "This property is owned by SelectingItemsControl, but protected there. ListBox changes its visibility.")]
-        public static readonly new StyledProperty<SelectionMode> SelectionModeProperty = 
+        public static readonly new StyledProperty<SelectionMode> SelectionModeProperty =
             SelectingItemsControl.SelectionModeProperty;
 
         private IScrollable? _scroll;
@@ -121,41 +125,34 @@ namespace Avalonia.Controls
             return NeedsContainer<ListBoxItem>(item, out recycleKey);
         }
 
-        /// <inheritdoc/>
-        protected override void OnGotFocus(GotFocusEventArgs e)
+        protected override void OnKeyDown(KeyEventArgs e)
         {
-            base.OnGotFocus(e);
+            var hotkeys = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
 
-            if (e.NavigationMethod == NavigationMethod.Directional)
+            if (e.Key.ToNavigationDirection() is { } direction)
+            {
+                e.Handled |= MoveSelection(
+                    direction,
+                    WrapSelection,
+                    e.KeyModifiers.HasAllFlags(KeyModifiers.Shift));
+            }
+            else if (SelectionMode.HasAllFlags(SelectionMode.Multiple) &&
+                hotkeys is not null && hotkeys.SelectAll.Any(x => x.Matches(e)))
             {
-                e.Handled = UpdateSelectionFromEventSource(
+                Selection.SelectAll();
+                e.Handled = true;
+            }
+            else if (e.Key == Key.Space || e.Key == Key.Enter)
+            {
+                var toggle = hotkeys is not null && e.KeyModifiers.HasAllFlags(hotkeys.CommandModifiers);
+                e.Handled |= UpdateSelectionFromEventSource(
                     e.Source,
                     true,
-                    e.KeyModifiers.HasAllFlags(KeyModifiers.Shift),
-                    e.KeyModifiers.HasAllFlags(KeyModifiers.Control),
-                    fromFocus: true);
+                    e.KeyModifiers.HasFlag(KeyModifiers.Shift),
+                    toggle);
             }
-        }
-
-        /// <inheritdoc/>
-        protected override void OnPointerPressed(PointerPressedEventArgs e)
-        {
-            base.OnPointerPressed(e);
 
-            if (e.Source is Visual source)
-            {
-                var point = e.GetCurrentPoint(source);
-
-                if (point.Properties.IsLeftButtonPressed || point.Properties.IsRightButtonPressed)
-                {
-                    e.Handled = UpdateSelectionFromEventSource(
-                        e.Source,
-                        true,
-                        e.KeyModifiers.HasAllFlags(KeyModifiers.Shift),
-                        e.KeyModifiers.HasAllFlags(AvaloniaLocator.Current.GetRequiredService<PlatformHotkeyConfiguration>().CommandModifiers),
-                        point.Properties.IsRightButtonPressed);
-                }
-            }
+            base.OnKeyDown(e);
         }
 
         protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -163,5 +160,18 @@ namespace Avalonia.Controls
             base.OnApplyTemplate(e);
             Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
         }
+
+        internal bool UpdateSelectionFromPointerEvent(Control source, PointerEventArgs e)
+        {
+            var hotkeys = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
+            var toggle = hotkeys is not null && e.KeyModifiers.HasAllFlags(hotkeys.CommandModifiers);
+
+            return UpdateSelectionFromEventSource(
+                source,
+                true,
+                e.KeyModifiers.HasAllFlags(KeyModifiers.Shift),
+                toggle,
+                e.GetCurrentPoint(source).Properties.IsRightButtonPressed);
+        }
     }
 }

+ 63 - 0
src/Avalonia.Controls/ListBoxItem.cs

@@ -2,6 +2,8 @@ using Avalonia.Automation.Peers;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Platform;
 
 namespace Avalonia.Controls
 {
@@ -17,6 +19,9 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<bool> IsSelectedProperty =
             SelectingItemsControl.IsSelectedProperty.AddOwner<ListBoxItem>();
 
+        private static readonly Point s_invalidPoint = new Point(double.NaN, double.NaN);
+        private Point _pointerDownPoint = s_invalidPoint;
+
         /// <summary>
         /// Initializes static members of the <see cref="ListBoxItem"/> class.
         /// </summary>
@@ -40,5 +45,63 @@ namespace Avalonia.Controls
         {
             return new ListItemAutomationPeer(this);
         }
+
+        protected override void OnPointerPressed(PointerPressedEventArgs e)
+        {
+            base.OnPointerPressed(e);
+
+            _pointerDownPoint = s_invalidPoint;
+
+            if (e.Handled)
+                return;
+
+            if (!e.Handled && ItemsControl.ItemsControlFromItemContaner(this) is ListBox owner)
+            {
+                var p = e.GetCurrentPoint(this);
+
+                if (p.Properties.PointerUpdateKind is PointerUpdateKind.LeftButtonPressed or 
+                    PointerUpdateKind.RightButtonPressed)
+                {
+                    if (p.Pointer.Type == PointerType.Mouse)
+                    {
+                        // If the pressed point comes from a mouse, perform the selection immediately.
+                        e.Handled = owner.UpdateSelectionFromPointerEvent(this, e);
+                    }
+                    else
+                    {
+                        // Otherwise perform the selection when the pointer is released as to not
+                        // interfere with gestures.
+                        _pointerDownPoint = p.Position;
+                        e.Handled = true;
+                    }
+                }
+            }
+        }
+
+        protected override void OnPointerReleased(PointerReleasedEventArgs e)
+        {
+            base.OnPointerReleased(e);
+
+            if (!e.Handled && 
+                !double.IsNaN(_pointerDownPoint.X) &&
+                e.InitialPressMouseButton is MouseButton.Left or MouseButton.Right)
+            {
+                var point = e.GetCurrentPoint(this);
+                var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
+                var tapSize = settings?.GetTapSize(point.Pointer.Type) ?? new Size(4, 4);
+                var tapRect = new Rect(_pointerDownPoint, new Size())
+                    .Inflate(new Thickness(tapSize.Width, tapSize.Height));
+
+                if (tapRect.ContainsExclusive(point.Position) &&
+                    ItemsControl.ItemsControlFromItemContaner(this) is ListBox owner)
+                {
+                    if (owner.UpdateSelectionFromPointerEvent(this, e))
+                        e.Handled = true;
+                }
+            }
+
+            _pointerDownPoint = s_invalidPoint;
+        }
+
     }
 }

+ 14 - 37
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -618,38 +618,6 @@ namespace Avalonia.Controls.Primitives
             base.OnTextInput(e);
         }
 
-        /// <inheritdoc />
-        protected override void OnKeyDown(KeyEventArgs e)
-        {
-            base.OnKeyDown(e);
-
-            if (!e.Handled)
-            {
-                var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
-
-                if (keymap is null)
-                    return;
-
-                bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
-
-                if (ItemCount > 0 &&
-                    Match(keymap.SelectAll) &&
-                    SelectionMode.HasAllFlags(SelectionMode.Multiple))
-                {
-                    Selection.SelectAll();
-                    e.Handled = true;
-                }
-                else if (e.Key == Key.Space || e.Key == Key.Enter)
-                {
-                    UpdateSelectionFromEventSource(
-                          e.Source,
-                          true,
-                          e.KeyModifiers.HasFlag(KeyModifiers.Shift),
-                          e.KeyModifiers.HasFlag(KeyModifiers.Control));
-                }
-            }
-        }
-
         /// <inheritdoc />
         protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
         {
@@ -729,11 +697,15 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         /// <param name="direction">The direction to move.</param>
         /// <param name="wrap">Whether to wrap when the selection reaches the first or last item.</param>
+        /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
         /// <returns>True if the selection was moved; otherwise false.</returns>
-        protected bool MoveSelection(NavigationDirection direction, bool wrap)
+        protected bool MoveSelection(
+            NavigationDirection direction,
+            bool wrap = false,
+            bool rangeModifier = false)
         {
-            var from = SelectedIndex != -1 ? ContainerFromIndex(SelectedIndex) : null;
-            return MoveSelection(from, direction, wrap);
+            var from = ContainerFromIndex(Selection.AnchorIndex);
+            return MoveSelection(from, direction, wrap, rangeModifier);
         }
 
         /// <summary>
@@ -742,8 +714,13 @@ namespace Avalonia.Controls.Primitives
         /// <param name="from">The container which serves as a starting point for the movement.</param>
         /// <param name="direction">The direction to move.</param>
         /// <param name="wrap">Whether to wrap when the selection reaches the first or last item.</param>
+        /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
         /// <returns>True if the selection was moved; otherwise false.</returns>
-        protected bool MoveSelection(Control? from, NavigationDirection direction, bool wrap)
+        protected bool MoveSelection(
+            Control? from,
+            NavigationDirection direction,
+            bool wrap = false,
+            bool rangeModifier = false)
         {
             if (Presenter?.Panel is INavigableContainer container &&
                 GetNextControl(container, direction, from, wrap) is Control next)
@@ -752,7 +729,7 @@ namespace Avalonia.Controls.Primitives
 
                 if (index != -1)
                 {
-                    SelectedIndex = index;
+                    UpdateSelection(index, true, rangeModifier);
                     return true;
                 }
             }

+ 5 - 5
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@@ -298,11 +298,11 @@ namespace Avalonia.Controls.UnitTests
 
                 Assert.Equal(false, item.IsSelected);
 
-                RaisePressedEvent(target, item, MouseButton.Left);
+                RaisePressedEvent(item, MouseButton.Left);
 
                 Assert.Equal(true, item.IsSelected);
 
-                RaisePressedEvent(target, item, MouseButton.Left);
+                RaisePressedEvent(item, MouseButton.Left);
 
                 Assert.Equal(false, item.IsSelected);
             }
@@ -328,9 +328,9 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
-        private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton)
+        private void RaisePressedEvent(ListBoxItem item, MouseButton mouseButton)
         {
-            _mouse.Click(listBox, item, mouseButton);
+            _mouse.Click(item, item, mouseButton);
         }
 
         [Fact]
@@ -776,7 +776,7 @@ namespace Avalonia.Controls.UnitTests
 
                 first.Focus();
 
-                RaisePressedEvent(target, first, MouseButton.Left);
+                RaisePressedEvent(first, MouseButton.Left);
                 Assert.Equal(true, first.IsSelected);
 
                 RaiseKeyEvent(target, Key.Up);

+ 0 - 70
tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs

@@ -17,76 +17,6 @@ namespace Avalonia.Controls.UnitTests
     {
         private MouseTestHelper _helper = new MouseTestHelper();
 
-        [Fact]
-        public void Focusing_Item_With_Shift_And_Arrow_Key_Should_Add_To_Selection()
-        {
-            var target = new ListBox
-            {
-                Template = new FuncControlTemplate(CreateListBoxTemplate),
-                ItemsSource = new[] { "Foo", "Bar", "Baz " },
-                SelectionMode = SelectionMode.Multiple
-            };
-
-            ApplyTemplate(target);
-
-            target.SelectedItem = "Foo";
-
-            target.Presenter.Panel.Children[1].RaiseEvent(new GotFocusEventArgs
-            {
-                NavigationMethod = NavigationMethod.Directional,
-                KeyModifiers = KeyModifiers.Shift
-            });
-
-            Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems);
-        }
-
-        [Fact]
-        public void Focusing_Item_With_Ctrl_And_Arrow_Key_Should_Not_Add_To_Selection()
-        {
-            var target = new ListBox
-            {
-                Template = new FuncControlTemplate(CreateListBoxTemplate),
-                ItemsSource = new[] { "Foo", "Bar", "Baz " },
-                SelectionMode = SelectionMode.Multiple
-            };
-
-            ApplyTemplate(target);
-
-            target.SelectedItem = "Foo";
-
-            target.Presenter.Panel.Children[1].RaiseEvent(new GotFocusEventArgs
-            {
-                NavigationMethod = NavigationMethod.Directional,
-                KeyModifiers = KeyModifiers.Control
-            });
-
-            Assert.Equal(new[] { "Foo" }, target.SelectedItems);
-        }
-
-        [Fact]
-        public void Focusing_Selected_Item_With_Ctrl_And_Arrow_Key_Should_Not_Remove_From_Selection()
-        {
-            var target = new ListBox
-            {
-                Template = new FuncControlTemplate(CreateListBoxTemplate),
-                ItemsSource = new[] { "Foo", "Bar", "Baz " },
-                SelectionMode = SelectionMode.Multiple
-            };
-
-            ApplyTemplate(target);
-
-            target.SelectedItems.Add("Foo");
-            target.SelectedItems.Add("Bar");
-
-            target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs
-            {
-                NavigationMethod = NavigationMethod.Directional,
-                KeyModifiers = KeyModifiers.Control
-            });
-
-            Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems);
-        }
-
         [Fact]
         public void Shift_Selecting_From_No_Selection_Selects_From_Start()
         {

+ 0 - 39
tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs

@@ -39,45 +39,6 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(-1, target.SelectedIndex);
         }
 
-        [Fact]
-        public void Focusing_Item_With_Arrow_Key_Should_Select_It()
-        {
-            var target = new ListBox
-            {
-                Template = new FuncControlTemplate(CreateListBoxTemplate),
-                ItemsSource = new[] { "Foo", "Bar", "Baz " },
-            };
-
-            ApplyTemplate(target);
-
-            target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs
-            {
-                NavigationMethod = NavigationMethod.Directional,
-            });
-
-            Assert.Equal(0, target.SelectedIndex);
-        }
-
-        [Fact]
-        public void Focusing_Item_With_Arrow_Key_And_Ctrl_Pressed_Should_Not_Select_It()
-        {
-            var target = new ListBox
-            {
-                Template = new FuncControlTemplate(CreateListBoxTemplate),
-                ItemsSource = new[] { "Foo", "Bar", "Baz " },
-            };
-
-            ApplyTemplate(target);
-
-            target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs
-            {
-                NavigationMethod = NavigationMethod.Directional,
-                KeyModifiers = KeyModifiers.Control
-            });
-
-            Assert.Equal(-1, target.SelectedIndex);
-        }
-
         [Fact]
         public void Pressing_Space_On_Focused_Item_With_Ctrl_Pressed_Should_Select_It()
         {