diff --git a/Perspex.Controls/Control.cs b/Perspex.Controls/Control.cs index 41dcb6aab1..7cc3545838 100644 --- a/Perspex.Controls/Control.cs +++ b/Perspex.Controls/Control.cs @@ -300,30 +300,6 @@ namespace Perspex.Controls } } - /// - /// Tries to being the control into view. - /// - public void BringIntoView() - { - this.BringIntoView(new Rect(this.Bounds.Size)); - } - - /// - /// Tries to being the specified area on the control into view. - /// - /// The area of the control to being into view. - public void BringIntoView(Rect rect) - { - var ev = new RequestBringIntoViewEventArgs - { - RoutedEvent = RequestBringIntoViewEvent, - TargetObject = this, - TargetRect = rect, - }; - - this.RaiseEvent(ev); - } - /// /// Sets the control's logical parent. /// diff --git a/Perspex.Controls/ControlExtensions.cs b/Perspex.Controls/ControlExtensions.cs index cb13407701..d490708573 100644 --- a/Perspex.Controls/ControlExtensions.cs +++ b/Perspex.Controls/ControlExtensions.cs @@ -6,18 +6,49 @@ namespace Perspex.Controls { - using System; - using System.Collections.Generic; using System.Linq; - using Perspex.Controls; - using Perspex.Controls.Primitives; using Perspex.LogicalTree; using Perspex.Styling; - using Perspex.VisualTree; + /// + /// Adds common functionality to . + /// public static class ControlExtensions { - public static T FindControl(this Control control, string name) where T : Control + /// + /// Tries to being the control into view. + /// + /// The control. + public static void BringIntoView(this IControl control) + { + control.BringIntoView(new Rect(control.Bounds.Size)); + } + + /// + /// Tries to being the control into view. + /// + /// The control. + /// The area of the control to being into view. + public static void BringIntoView(this IControl control, Rect rect) + { + var ev = new RequestBringIntoViewEventArgs + { + RoutedEvent = Control.RequestBringIntoViewEvent, + TargetObject = control, + TargetRect = rect, + }; + + control.RaiseEvent(ev); + } + + /// + /// Finds the named control in the specified control. + /// + /// The type of the control to find. + /// The control. + /// The name of the control to find. + /// The control or null if not found. + public static T FindControl(this IControl control, string name) where T : IControl { return control.GetLogicalDescendents() .OfType() diff --git a/Perspex.Controls/MenuItem.cs b/Perspex.Controls/MenuItem.cs index e539c1dea3..72bee4959f 100644 --- a/Perspex.Controls/MenuItem.cs +++ b/Perspex.Controls/MenuItem.cs @@ -8,6 +8,7 @@ namespace Perspex.Controls { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Windows.Input; using Perspex.Controls.Mixins; @@ -73,6 +74,15 @@ namespace Perspex.Controls public static readonly RoutedEvent SubmenuOpenedEvent = RoutedEvent.Register(nameof(SubmenuOpened), RoutingStrategies.Bubble); + /// + /// The default value for the property. + /// + private static readonly ItemsPanelTemplate DefaultPanel = + new ItemsPanelTemplate(() => new StackPanel + { + [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, + }); + /// /// The timer used to display submenus. /// @@ -90,6 +100,7 @@ namespace Perspex.Controls { SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(true); + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); ClickEvent.AddClassHandler(x => x.OnClick); SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); IsSubMenuOpenProperty.Changed.AddClassHandler(x => x.SubMenuOpenChanged); @@ -222,21 +233,6 @@ namespace Perspex.Controls switch (e.Key) { - case Key.Up: - passStraightToParent = !this.IsSubMenuOpen; - break; - - case Key.Down: - if (this.IsTopLevel && this.HasSubMenu && !this.IsSubMenuOpen) - { - this.SelectedIndex = 0; - this.IsSubMenuOpen = true; - e.Handled = true; - } - - passStraightToParent = !this.IsSubMenuOpen; - break; - case Key.Left: if (!this.IsTopLevel && this.IsSubMenuOpen) { diff --git a/Perspex.Controls/Mixins/SelectableMixin.cs b/Perspex.Controls/Mixins/SelectableMixin.cs index 95cf398677..7d4ee0f5e1 100644 --- a/Perspex.Controls/Mixins/SelectableMixin.cs +++ b/Perspex.Controls/Mixins/SelectableMixin.cs @@ -26,6 +26,9 @@ namespace Perspex.Controls.Mixins /// /// Adds a 'selected' class to selected controls. /// + /// + /// Requests that the control is scrolled into view when focused. + /// /// /// /// Mixins apply themselves to classes and not instances, and as such should be created in @@ -53,6 +56,11 @@ namespace Perspex.Controls.Mixins if ((bool)x.NewValue) { sender.Classes.Add("selected"); + + if (((IVisual)sender).IsAttachedToVisualTree) + { + sender.BringIntoView(); + } } else { diff --git a/Perspex.Controls/Presenters/ItemsPresenter.cs b/Perspex.Controls/Presenters/ItemsPresenter.cs index c1ffc374b1..522e015241 100644 --- a/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -141,6 +141,11 @@ namespace Perspex.Controls.Presenters this.Panel = this.ItemsPanel.Build(); this.Panel.SetValue(TemplatedParentProperty, this.TemplatedParent); + if (!this.Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) + { + KeyboardNavigation.SetDirectionalNavigation(this.Panel, KeyboardNavigationMode.Contained); + } + this.AddVisualChild(this.Panel); var logicalHost = this.FindReparentingHost(); diff --git a/Perspex.Controls/TopLevel.cs b/Perspex.Controls/TopLevel.cs index 74bf967a7a..78feabbdc5 100644 --- a/Perspex.Controls/TopLevel.cs +++ b/Perspex.Controls/TopLevel.cs @@ -149,6 +149,16 @@ namespace Perspex.Controls this.renderManager.RenderNeeded.Subscribe(_ => this.HandleRenderNeeded()); } + if (this.keyboardNavigationHandler != null) + { + this.keyboardNavigationHandler.SetOwner(this); + } + + if (this.accessKeyHandler != null) + { + this.accessKeyHandler.SetOwner(this); + } + styler?.ApplyStyles(this); this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => this.PlatformImpl.ClientSize = x); diff --git a/Perspex.Input/IKeyboardNavigationHandler.cs b/Perspex.Input/IKeyboardNavigationHandler.cs index 37781e1cd0..fb997e3875 100644 --- a/Perspex.Input/IKeyboardNavigationHandler.cs +++ b/Perspex.Input/IKeyboardNavigationHandler.cs @@ -21,15 +21,10 @@ namespace Perspex.Input void SetOwner(IInputRoot owner); /// - /// Moves the focus to the next control in tab order. + /// Moves the focus in the specified direction. /// /// The current element. - void TabNext(IInputElement element); - - /// - /// Moves the focus to the previous control in tab order. - /// - /// The current element. - void TabPrevious(IInputElement element); + /// The direction to move. + void Move(IInputElement element, FocusNavigationDirection direction); } } \ No newline at end of file diff --git a/Perspex.Input/KeyboardNavigationHandler.cs b/Perspex.Input/KeyboardNavigationHandler.cs index f33913843b..b9a5e76655 100644 --- a/Perspex.Input/KeyboardNavigationHandler.cs +++ b/Perspex.Input/KeyboardNavigationHandler.cs @@ -98,30 +98,15 @@ namespace Perspex.Input } /// - /// Moves the focus to the next control in tab order. + /// Moves the focus in the specified direction. /// /// The current element. - public void TabNext(IInputElement element) + /// The direction to move. + public void Move(IInputElement element, FocusNavigationDirection direction) { Contract.Requires(element != null); - var next = GetNext(element, FocusNavigationDirection.Next); - - if (next != null) - { - FocusManager.Instance.Focus(next, true); - } - } - - /// - /// Moves the focus to the previous control in tab order. - /// - /// The current element. - public void TabPrevious(IInputElement element) - { - Contract.Requires(element != null); - - var next = GetNext(element, FocusNavigationDirection.Previous); + var next = GetNext(element, direction); if (next != null) { @@ -312,15 +297,34 @@ namespace Perspex.Input { var current = FocusManager.Instance.Current; - if (e.Key == Key.Tab && current != null) + if (current != null) { - if ((KeyboardDevice.Instance.Modifiers & ModifierKeys.Shift) == 0) + FocusNavigationDirection? direction = null; + + switch (e.Key) { - this.TabNext(current); + case Key.Tab: + direction = (KeyboardDevice.Instance.Modifiers & ModifierKeys.Shift) == 0 ? + FocusNavigationDirection.Next : FocusNavigationDirection.Previous; + break; + case Key.Up: + direction = FocusNavigationDirection.Up; + break; + case Key.Down: + direction = FocusNavigationDirection.Down; + break; + case Key.Left: + direction = FocusNavigationDirection.Left; + break; + case Key.Right: + direction = FocusNavigationDirection.Right; + break; } - else + + if (direction.HasValue) { - this.TabPrevious(current); + this.Move(current, direction.Value); + e.Handled = true; } } } diff --git a/Perspex.SceneGraph/IVisual.cs b/Perspex.SceneGraph/IVisual.cs index e4594e5e00..7b9554c3ae 100644 --- a/Perspex.SceneGraph/IVisual.cs +++ b/Perspex.SceneGraph/IVisual.cs @@ -32,6 +32,11 @@ namespace Perspex /// bool ClipToBounds { get; } + /// + /// Gets a value indicating whether this scene graph node is attached to a visual root. + /// + bool IsAttachedToVisualTree { get; } + /// /// Gets a value indicating whether this scene graph node and all its parents are visible. /// diff --git a/Perspex.SceneGraph/Visual.cs b/Perspex.SceneGraph/Visual.cs index 142a781b8e..9c119cc802 100644 --- a/Perspex.SceneGraph/Visual.cs +++ b/Perspex.SceneGraph/Visual.cs @@ -83,6 +83,11 @@ namespace Perspex /// private Visual visualParent; + /// + /// Whether the element is attached to the visual tree. + /// + private bool isAttachedToVisualTree; + /// /// The logger for visual-level events. /// @@ -185,6 +190,15 @@ namespace Perspex set { this.SetValue(ZIndexProperty, value); } } + /// + /// Gets a value indicating whether this scene graph node is attached to a visual root. + /// + bool IVisual.IsAttachedToVisualTree + { + get { return this.isAttachedToVisualTree; } + } + + /// /// Gets the scene graph node's child nodes. /// @@ -487,6 +501,7 @@ namespace Perspex { this.visualLogger.Verbose("Attached to visual tree"); + this.isAttachedToVisualTree = true; this.OnAttachedToVisualTree(root); if (this.visualChildren != null) @@ -507,6 +522,7 @@ namespace Perspex { this.visualLogger.Verbose("Detached from visual tree"); + this.isAttachedToVisualTree = false; this.OnDetachedFromVisualTree(root); if (this.visualChildren != null)