Browse Source

Merge pull request #2122 from donandren/prs/dropdownimprovements

DropDown improvements
Steven Kirk 7 years ago
parent
commit
2731155511

+ 80 - 7
src/Avalonia.Controls/DropDown.cs

@@ -2,9 +2,12 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Linq;
 using Avalonia.Controls.Generators;
+using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Shapes;
+using Avalonia.Controls.Templates;
 using Avalonia.Input;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
@@ -12,12 +15,17 @@ using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
 {
-
     /// <summary>
     /// A drop-down list control.
     /// </summary>
     public class DropDown : SelectingItemsControl
     {
+        /// <summary>
+        /// The default value for the <see cref="ItemsControl.ItemsPanel"/> property.
+        /// </summary>
+        private static readonly FuncTemplate<IPanel> DefaultPanel =
+            new FuncTemplate<IPanel>(() => new VirtualizingStackPanel());
+
         /// <summary>
         /// Defines the <see cref="IsDropDownOpen"/> property.
         /// </summary>
@@ -39,6 +47,12 @@ namespace Avalonia.Controls
         public static readonly DirectProperty<DropDown, object> SelectionBoxItemProperty =
             AvaloniaProperty.RegisterDirect<DropDown, object>(nameof(SelectionBoxItem), o => o.SelectionBoxItem);
 
+        /// <summary>
+        /// Defines the <see cref="VirtualizationMode"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ItemVirtualizationMode> VirtualizationModeProperty =
+            ItemsPresenter.VirtualizationModeProperty.AddOwner<DropDown>();
+
         private bool _isDropDownOpen;
         private Popup _popup;
         private object _selectionBoxItem;
@@ -48,6 +62,7 @@ namespace Avalonia.Controls
         /// </summary>
         static DropDown()
         {
+            ItemsPanelProperty.OverrideDefaultValue<DropDown>(DefaultPanel);
             FocusableProperty.OverrideDefaultValue<DropDown>(true);
             SelectedItemProperty.Changed.AddClassHandler<DropDown>(x => x.SelectedItemChanged);
             KeyDownEvent.AddClassHandler<DropDown>(x => x.OnKeyDown, Interactivity.RoutingStrategies.Tunnel);
@@ -80,6 +95,15 @@ namespace Avalonia.Controls
             set { SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the virtualization mode for the items.
+        /// </summary>
+        public ItemVirtualizationMode VirtualizationMode
+        {
+            get { return GetValue(VirtualizationModeProperty); }
+            set { SetValue(VirtualizationModeProperty, value); }
+        }
+
         /// <inheritdoc/>
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
@@ -138,6 +162,16 @@ namespace Avalonia.Controls
                     e.Handled = true;
                 }
             }
+            else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 &&
+                      (e.Key == Key.Up || e.Key == Key.Down))
+            {
+                var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c));
+                if (firstChild != null)
+                {
+                    FocusManager.Instance?.Focus(firstChild, NavigationMethod.Directional);
+                    e.Handled = true;
+                }
+            }
         }
 
         /// <inheritdoc/>
@@ -159,6 +193,7 @@ namespace Avalonia.Controls
                     e.Handled = true;
                 }
             }
+
             base.OnPointerPressed(e);
         }
 
@@ -168,28 +203,65 @@ namespace Avalonia.Controls
             if (_popup != null)
             {
                 _popup.Opened -= PopupOpened;
+                _popup.Closed -= PopupClosed;
             }
 
             _popup = e.NameScope.Get<Popup>("PART_Popup");
             _popup.Opened += PopupOpened;
+            _popup.Closed += PopupClosed;
+
+            base.OnTemplateApplied(e);
         }
 
-        private void PopupOpened(object sender, EventArgs e)
+        internal void ItemFocused(DropDownItem dropDownItem)
         {
-            var selectedIndex = SelectedIndex;
+            if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
+            {
+                dropDownItem.BringIntoView();
+            }
+        }
 
-            if (selectedIndex != -1)
+        private void PopupClosed(object sender, EventArgs e)
+        {
+            if (CanFocus(this))
             {
-                var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
-                container?.Focus();
+                Focus();
             }
         }
 
+        private void PopupOpened(object sender, EventArgs e)
+        {
+            TryFocusSelectedItem();
+        }
+
         private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e)
         {
             UpdateSelectionBoxItem(e.NewValue);
+            TryFocusSelectedItem();
+        }
+
+        private void TryFocusSelectedItem()
+        {
+            var selectedIndex = SelectedIndex;
+            if (IsDropDownOpen && selectedIndex != -1)
+            {
+                var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
+
+                if(container == null && SelectedItems.Count > 0)
+                {
+                    ScrollIntoView(SelectedItems[0]);
+                    container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
+                }
+
+                if (container != null && CanFocus(container))
+                {
+                    container.Focus();
+                }
+            }
         }
 
+        private bool CanFocus(IControl control) => control.Focusable && control.IsEnabledCore && control.IsVisible;
+
         private void UpdateSelectionBoxItem(object item)
         {
             var contentControl = item as IContentControl;
@@ -219,7 +291,8 @@ namespace Avalonia.Controls
             }
             else
             {
-                SelectionBoxItem = item;
+                var selector = MemberSelector;
+                SelectionBoxItem = selector != null ? selector.Select(item) : item;
             }
         }
 

+ 4 - 28
src/Avalonia.Controls/DropDownItem.cs

@@ -2,43 +2,19 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Reactive.Linq;
 
 namespace Avalonia.Controls
 {
     /// <summary>
     /// A selectable item in a <see cref="DropDown"/>.
     /// </summary>
-    public class DropDownItem : ContentControl, ISelectable
+    public class DropDownItem : ListBoxItem
     {
-        /// <summary>
-        /// Defines the <see cref="IsSelected"/> property.
-        /// </summary>
-        public static readonly StyledProperty<bool> IsSelectedProperty =
-            AvaloniaProperty.Register<DropDownItem, bool>(nameof(IsSelected));
-
-        /// <summary>
-        /// Initializes static members of the <see cref="DropDownItem"/> class.
-        /// </summary>
-        static DropDownItem()
-        {
-            FocusableProperty.OverrideDefaultValue<DropDownItem>(true);
-        }
-
         public DropDownItem()
         {
-            this.GetObservable(DropDownItem.IsFocusedProperty).Subscribe(focused =>
-            {
-                PseudoClasses.Set(":selected", focused);                
-            });
-        }
-
-        /// <summary>
-        /// Gets or sets the selection state of the item.
-        /// </summary>
-        public bool IsSelected
-        {
-            get { return GetValue(IsSelectedProperty); }
-            set { SetValue(IsSelectedProperty, value); }
+            this.GetObservable(DropDownItem.IsFocusedProperty).Where(focused => focused)
+                .Subscribe(_ => (Parent as DropDown)?.ItemFocused(this));
         }
     }
 }

+ 6 - 3
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -431,9 +431,12 @@ namespace Avalonia.Controls.Primitives
             {
                 if (i.ContainerControl != null && i.Item != null)
                 {
-                    MarkContainerSelected(
-                        i.ContainerControl,
-                        SelectedItems.Contains(i.Item));
+                    var ms = MemberSelector;
+                    bool selected = ms == null ? 
+                        SelectedItems.Contains(i.Item) : 
+                        SelectedItems.OfType<object>().Any(v => Equals(ms.Select(v), i.Item));
+
+                    MarkContainerSelected(i.ContainerControl, selected);
                 }
             }
         }

+ 13 - 7
src/Avalonia.Themes.Default/DropDown.xaml

@@ -35,15 +35,21 @@
                    MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
                    MaxHeight="{TemplateBinding MaxDropDownHeight}"
                    PlacementTarget="{TemplateBinding}"
+                   ObeyScreenEdges="True"
                    StaysOpen="False">
               <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                       BorderThickness="1">
-                <ScrollViewer>
-                  <ItemsPresenter Name="PART_ItemsPresenter"
-                                  Items="{TemplateBinding Items}"
-                                  ItemTemplate="{TemplateBinding ItemTemplate}"
-                                  MemberSelector="{TemplateBinding MemberSelector}"/>
-                </ScrollViewer>
+                  <AdornerDecorator Margin="-1 -1 0 0">
+                      <ScrollViewer>
+                          <ItemsPresenter Name="PART_ItemsPresenter"
+                                          Items="{TemplateBinding Items}"
+                                          ItemsPanel="{TemplateBinding ItemsPanel}"
+                                          ItemTemplate="{TemplateBinding ItemTemplate}"
+                                          MemberSelector="{TemplateBinding MemberSelector}"
+                                          VirtualizationMode="{TemplateBinding VirtualizationMode}"
+                                  />
+                      </ScrollViewer>
+                  </AdornerDecorator>
               </Border>
             </Popup>
           </Grid>
@@ -54,4 +60,4 @@
   <Style Selector="DropDown:pointerover /template/ Border#border">
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}"/>
   </Style>
-</Styles>
+</Styles>

+ 1 - 1
src/Avalonia.Themes.Default/DropDownItem.xaml

@@ -18,7 +18,7 @@
       </ControlTemplate>
     </Setter>
   </Style>
-
+    
   <Style Selector="DropDownItem:pointerover /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ThemeControlHighlightMidBrush}"/>
   </Style>

+ 3 - 2
src/Avalonia.Themes.Default/FocusAdorner.xaml

@@ -3,7 +3,8 @@
     <FocusAdornerTemplate>
       <Rectangle Stroke="Black"
                  StrokeThickness="1"
-                 StrokeDashArray="1,2"/>
+                 StrokeDashArray="1,2"
+                 Margin="1"/>
     </FocusAdornerTemplate>
   </Setter>
-</Style>
+</Style>