From 9e2e266d3c3a85c079e11137482b8c717884c699 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 Jul 2018 09:54:00 +0200 Subject: [PATCH] Handle navigation in TreeView. --- src/Avalonia.Controls/ItemsControl.cs | 72 ++++++++++++------- src/Avalonia.Controls/TreeView.cs | 86 +++++++++++++++++++++++ src/Avalonia.Controls/TreeViewItem.cs | 2 +- src/Avalonia.Input/NavigationDirection.cs | 70 ++++++++++++++++++ 4 files changed, 202 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 998111e8b9..e2d2f0a516 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/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); } + /// + /// Handles directional navigation within the . + /// + /// The key events. 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() + .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(); + } + } } } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 2e1c011685..4575fa767b 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/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; + } + /// protected override void OnPointerPressed(PointerPressedEventArgs e) { diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 8af3333dd4..0886c05038 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/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. } } } diff --git a/src/Avalonia.Input/NavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs index fbaa7e74c7..406890b767 100644 --- a/src/Avalonia.Input/NavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -58,4 +58,74 @@ namespace Avalonia.Input /// PageDown, } + + public static class NavigationDirectionExtensions + { + /// + /// Checks whether a represents a tab movement. + /// + /// The direction. + /// + /// True if the direction represents a tab movement ( + /// or ); otherwise false. + /// + public static bool IsTab(this NavigationDirection direction) + { + return direction == NavigationDirection.Next || + direction == NavigationDirection.Previous; + } + + /// + /// Checks whether a represents a directional movement. + /// + /// The direction. + /// + /// True if the direction represents a directional movement (any value except + /// and ); + /// otherwise false. + /// + public static bool IsDirectional(this NavigationDirection direction) + { + return direction > NavigationDirection.Previous || + direction <= NavigationDirection.PageDown; + } + + /// + /// Converts a keypress into a . + /// + /// The key. + /// The keyboard modifiers. + /// + /// A if the keypress represents a navigation keypress. + /// + 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; + } + } + } }