Bläddra i källkod

Handle navigation in TreeView.

Steven Kirk 7 år sedan
förälder
incheckning
9e2e266d3c

+ 45 - 27
src/Avalonia.Controls/ItemsControl.cs

@@ -15,6 +15,7 @@ using Avalonia.Controls.Utils;
 using Avalonia.Input;
 using Avalonia.LogicalTree;
 using Avalonia.Metadata;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
 {
@@ -323,35 +324,37 @@ namespace Avalonia.Controls
             LogicalChildren.RemoveAll(toRemove);
         }
 
+        /// <summary>
+        /// Handles directional navigation within the <see cref="ItemsControl"/>.
+        /// </summary>
+        /// <param name="e">The key events.</param>
         protected override void OnKeyDown(KeyEventArgs e)
         {
-            if (Presenter?.Panel is INavigableContainer container)
+            var focus = FocusManager.Instance;
+            var direction = e.Key.ToNavigationDirection();
+            var container = Presenter?.Panel as INavigableContainer;
+
+            if (container == null ||
+                focus.Current == null ||
+                direction == null ||
+                direction.Value.IsTab())
             {
-                var focus = FocusManager.Instance;
-                var current = focus.Current;
-                NavigationDirection? direction = null;
+                return;
+            }
 
-                switch (e.Key)
-                {
-                    case Key.Up: direction = NavigationDirection.Up; break;
-                    case Key.Down: direction = NavigationDirection.Down; break;
-                    case Key.Left: direction = NavigationDirection.Left; break;
-                    case Key.Right: direction = NavigationDirection.Right; break;
-                    case Key.Home: direction = NavigationDirection.First; break;
-                    case Key.End: direction = NavigationDirection.Last; break;
-                    case Key.PageUp: direction = NavigationDirection.PageUp; break;
-                    case Key.PageDown: direction = NavigationDirection.PageDown; break;
-                }
+            var current = focus.Current
+                .GetSelfAndVisualAncestors()
+                .OfType<IInputElement>()
+                .FirstOrDefault(x => x.VisualParent == container);
 
-                if (direction != null && current != null)
-                {
-                    var next = container.GetControl(direction.Value, current);
+            if (current != null)
+            {
+                var next = container.GetControl(direction.Value, current);
 
-                    if (next != null)
-                    {
-                        focus.Focus(next, NavigationMethod.Directional);
-                        e.Handled = true;
-                    }
+                if (next != null)
+                {
+                    focus.Focus(next, NavigationMethod.Directional);
+                    e.Handled = true;
                 }
             }
 
@@ -370,6 +373,7 @@ namespace Avalonia.Controls
             var oldValue = e.OldValue as IEnumerable;
             var newValue = e.NewValue as IEnumerable;
 
+            UpdateItemCount();
             RemoveControlItemsFromLogicalChildren(oldValue);
             AddControlItemsToLogicalChildren(newValue);
             SubscribeToItems(newValue);
@@ -393,10 +397,8 @@ namespace Avalonia.Controls
                     RemoveControlItemsFromLogicalChildren(e.OldItems);
                     break;
             }
-            
-            int? count = (Items as IList)?.Count;
-            if (count != null)
-                ItemCount = (int)count;
+
+            UpdateItemCount();
 
             var collection = sender as ICollection;
             PseudoClasses.Set(":empty", collection == null || collection.Count == 0);
@@ -480,5 +482,21 @@ namespace Avalonia.Controls
                 // TODO: Rebuild the item containers.
             }
         }
+
+        private void UpdateItemCount()
+        {
+            if (Items == null)
+            {
+                ItemCount = 0;
+            }
+            else if (Items is IList list)
+            {
+                ItemCount = list.Count;
+            }
+            else
+            {
+                ItemCount = Items.Count();
+            }
+        }
     }
 }

+ 86 - 0
src/Avalonia.Controls/TreeView.cs

@@ -136,6 +136,92 @@ namespace Avalonia.Controls
             }
         }
 
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            var direction = e.Key.ToNavigationDirection();
+
+            if (direction?.IsDirectional() == true && !e.Handled)
+            {
+                if (SelectedItem != null)
+                {
+                    var next = GetContainerInDirection(
+                        GetContainerFromEventSource(e.Source) as TreeViewItem,
+                        direction.Value,
+                        true);
+
+                    if (next != null)
+                    {
+                        FocusManager.Instance.Focus(next, NavigationMethod.Directional);
+                        e.Handled = true;
+                    }
+                }
+                else
+                {
+                    SelectedItem = ElementAt(Items, 0);
+                }
+            }
+        }
+
+        private TreeViewItem GetContainerInDirection(
+            TreeViewItem from,
+            NavigationDirection direction,
+            bool intoChildren)
+        {
+            IItemContainerGenerator parentGenerator;
+
+            if (from?.Parent is TreeView treeView)
+            {
+                parentGenerator = treeView.ItemContainerGenerator;
+            }
+            else if (from?.Parent is TreeViewItem item)
+            {
+                parentGenerator = item.ItemContainerGenerator;
+            }
+            else
+            {
+                return null;
+            }
+
+            var index = parentGenerator.IndexFromContainer(from);
+            var parent = from.Parent as ItemsControl;
+            TreeViewItem result = null;
+
+            switch (direction)
+            {
+                case NavigationDirection.Up:
+                    if (index > 0)
+                    {
+                        var previous = (TreeViewItem)parentGenerator.ContainerFromIndex(index - 1);
+                        result = previous.IsExpanded ?
+                            (TreeViewItem)previous.ItemContainerGenerator.ContainerFromIndex(previous.ItemCount - 1) :
+                            previous;
+                    }
+                    else
+                    {
+                        result = from.Parent as TreeViewItem;
+                    }
+
+                    break;
+
+                case NavigationDirection.Down:
+                    if (from.IsExpanded && intoChildren)
+                    {
+                        result = (TreeViewItem)from.ItemContainerGenerator.ContainerFromIndex(0);
+                    }
+                    else if (index < parent?.ItemCount - 1)
+                    {
+                        result = (TreeViewItem)parentGenerator.ContainerFromIndex(index + 1);
+                    }
+                    else if (parent is TreeViewItem parentItem)
+                    {
+                        return GetContainerInDirection(parentItem, direction, false);
+                    }
+                    break;
+            }
+
+            return result;
+        }
+
         /// <inheritdoc/>
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         {

+ 1 - 1
src/Avalonia.Controls/TreeViewItem.cs

@@ -124,7 +124,7 @@ namespace Avalonia.Controls
                 }
             }
 
-            base.OnKeyDown(e);
+            // Don't call base.OnKeyDown - let events bubble up to containing TreeView.
         }
     }
 }

+ 70 - 0
src/Avalonia.Input/NavigationDirection.cs

@@ -58,4 +58,74 @@ namespace Avalonia.Input
         /// </summary>
         PageDown,
     }
+
+    public static class NavigationDirectionExtensions
+    {
+        /// <summary>
+        /// Checks whether a <see cref="NavigationDirection"/> represents a tab movement.
+        /// </summary>
+        /// <param name="direction">The direction.</param>
+        /// <returns>
+        /// True if the direction represents a tab movement (<see cref="NavigationDirection.Next"/>
+        /// or <see cref="NavigationDirection.Previous"/>); otherwise false.
+        /// </returns>
+        public static bool IsTab(this NavigationDirection direction)
+        {
+            return direction == NavigationDirection.Next ||
+                direction == NavigationDirection.Previous;
+        }
+
+        /// <summary>
+        /// Checks whether a <see cref="NavigationDirection"/> represents a directional movement.
+        /// </summary>
+        /// <param name="direction">The direction.</param>
+        /// <returns>
+        /// True if the direction represents a directional movement (any value except 
+        /// <see cref="NavigationDirection.Next"/> and <see cref="NavigationDirection.Previous"/>);
+        /// otherwise false.
+        /// </returns>
+        public static bool IsDirectional(this NavigationDirection direction)
+        {
+            return direction > NavigationDirection.Previous ||
+                direction <= NavigationDirection.PageDown;
+        }
+
+        /// <summary>
+        /// Converts a keypress into a <see cref="NavigationDirection"/>.
+        /// </summary>
+        /// <param name="key">The key.</param>
+        /// <param name="modifiers">The keyboard modifiers.</param>
+        /// <returns>
+        /// A <see cref="NavigationDirection"/> if the keypress represents a navigation keypress.
+        /// </returns>
+        public static NavigationDirection? ToNavigationDirection(
+            this Key key,
+            InputModifiers modifiers = InputModifiers.None)
+        {
+            switch (key)
+            {
+                case Key.Tab:
+                    return (modifiers & InputModifiers.Shift) != 0 ?
+                        NavigationDirection.Next : NavigationDirection.Previous;
+                case Key.Up:
+                    return NavigationDirection.Up;
+                case Key.Down:
+                    return NavigationDirection.Down;
+                case Key.Left:
+                    return NavigationDirection.Left;
+                case Key.Right:
+                    return NavigationDirection.Right;
+                case Key.Home:
+                    return NavigationDirection.First;
+                case Key.End:
+                    return NavigationDirection.Last;
+                case Key.PageUp:
+                    return NavigationDirection.PageUp;
+                case Key.PageDown:
+                    return NavigationDirection.PageDown;
+                default:
+                    return null;
+            }
+        }
+    }
 }