diff --git a/Perspex.Application/Application.cs b/Perspex.Application/Application.cs index 1057c61cea..2f30f604bd 100644 --- a/Perspex.Application/Application.cs +++ b/Perspex.Application/Application.cs @@ -70,12 +70,6 @@ namespace Perspex private set; } - public KeyboardNavigation KeyboardNavigation - { - get; - private set; - } - public Styles Styles { get; @@ -91,17 +85,17 @@ namespace Perspex protected virtual void RegisterServices() { + var keyboardNavigation = new KeyboardNavigation(); + this.FocusManager = new FocusManager(); this.InputManager = new InputManager(); - this.KeyboardNavigation = new KeyboardNavigation(); Locator.CurrentMutable.Register(() => this, typeof(IGlobalDataTemplates)); Locator.CurrentMutable.Register(() => this, typeof(IGlobalStyles)); Locator.CurrentMutable.Register(() => this.FocusManager, typeof(IFocusManager)); Locator.CurrentMutable.Register(() => this.InputManager, typeof(IInputManager)); - Locator.CurrentMutable.Register(() => this.KeyboardNavigation, typeof(IKeyboardNavigation)); + Locator.CurrentMutable.Register(() => keyboardNavigation, typeof(IKeyboardNavigation)); Locator.CurrentMutable.Register(() => this.styler, typeof(IStyler)); - Locator.CurrentMutable.Register(() => new LayoutManager(), typeof(ILayoutManager)); Locator.CurrentMutable.Register(() => new RenderManager(), typeof(IRenderManager)); } diff --git a/Perspex.Base/PerspexProperty.cs b/Perspex.Base/PerspexProperty.cs index 2484e8db31..aa8ba55285 100644 --- a/Perspex.Base/PerspexProperty.cs +++ b/Perspex.Base/PerspexProperty.cs @@ -39,11 +39,6 @@ namespace Perspex /// private Subject changed = new Subject(); - /// - /// The coerce function. - /// - private Func coerce; - /// /// Initializes a new instance of the class. /// @@ -183,7 +178,6 @@ namespace Perspex bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, Func coerce = null) - where TOwner : PerspexObject { Contract.Requires(name != null); diff --git a/Perspex.Controls/ItemsControl.cs b/Perspex.Controls/ItemsControl.cs index 02358935d2..312ed27b88 100644 --- a/Perspex.Controls/ItemsControl.cs +++ b/Perspex.Controls/ItemsControl.cs @@ -35,8 +35,6 @@ namespace Perspex.Controls private PerspexReadOnlyListView logicalChildren = new PerspexReadOnlyListView(x => (ILogical)x); - private ItemsPresenter presenter; - public ItemsControl() { this.GetObservableWithHistory(ItemsProperty).Subscribe(this.ItemsChanged); @@ -76,6 +74,12 @@ namespace Perspex.Controls } } + protected ItemsPresenter Presenter + { + get; + private set; + } + protected virtual ItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator(this); @@ -83,11 +87,11 @@ namespace Perspex.Controls protected override void OnTemplateApplied() { - this.presenter = this.FindTemplateChild("itemsPresenter"); + this.Presenter = this.FindTemplateChild("itemsPresenter"); - if (this.presenter != null) + if (this.Presenter != null) { - this.logicalChildren.Source = ((IVisual)this.presenter.Panel).VisualChildren; + this.logicalChildren.Source = ((IVisual)this.Presenter.Panel).VisualChildren; } } diff --git a/Perspex.Controls/Presenters/ItemsPresenter.cs b/Perspex.Controls/Presenters/ItemsPresenter.cs index a79ee9e4ab..51652a0ed7 100644 --- a/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -11,6 +11,7 @@ namespace Perspex.Controls.Presenters using System.Collections.Specialized; using System.Reactive.Linq; using Perspex.Controls.Generators; + using Perspex.Input; using Perspex.Styling; public class ItemsPresenter : Control, IVisual, IPresenter, ITemplatedControl @@ -71,6 +72,7 @@ namespace Perspex.Controls.Presenters this.ClearVisualChildren(); this.Panel = this.ItemsPanel.Build(); this.Panel.TemplatedParent = this; + KeyboardNavigation.SetTabNavigation(this.Panel, KeyboardNavigationMode.Once); ((IItemsPanel)this.Panel).ChildLogicalParent = this.TemplatedParent as ILogical; this.AddVisualChild(this.Panel); this.createdPanel = true; diff --git a/Perspex.Controls/Primitives/SelectingItemsControl.cs b/Perspex.Controls/Primitives/SelectingItemsControl.cs index c18e9cffcf..63956b179a 100644 --- a/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -72,9 +72,11 @@ namespace Perspex.Controls.Primitives switch (direction) { case FocusNavigationDirection.Up: + case FocusNavigationDirection.Left: offset = -1; break; case FocusNavigationDirection.Down: + case FocusNavigationDirection.Right: offset = 1; break; } @@ -94,6 +96,8 @@ namespace Perspex.Controls.Primitives protected override void OnKeyDown(KeyEventArgs e) { + base.OnKeyDown(e); + switch (e.Key) { case Key.Up: @@ -143,6 +147,11 @@ namespace Perspex.Controls.Primitives this.ItemContainerGenerator.GetContainerForItem(selected) : null; + if (this.Presenter != null && this.Presenter.Panel != null) + { + KeyboardNavigation.SetTabOnceActiveElement(this.Presenter.Panel, selectedContainer); + } + foreach (var item in containers) { item.IsSelected = item == selectedContainer; diff --git a/Perspex.Controls/TabItem.cs b/Perspex.Controls/TabItem.cs index 94828fbdb5..059995ae6e 100644 --- a/Perspex.Controls/TabItem.cs +++ b/Perspex.Controls/TabItem.cs @@ -15,6 +15,7 @@ namespace Perspex.Controls static TabItem() { + FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); Control.AffectsRender(IsSelectedProperty); Control.PseudoClass(IsSelectedProperty, ":selected"); } diff --git a/Perspex.Input/FocusManager.cs b/Perspex.Input/FocusManager.cs index 9750c05cff..2536345232 100644 --- a/Perspex.Input/FocusManager.cs +++ b/Perspex.Input/FocusManager.cs @@ -25,8 +25,7 @@ namespace Perspex.Input public IInputElement Current { - get; - private set; + get { return KeyboardDevice.Instance.FocusedElement; } } public IFocusScope Scope @@ -84,29 +83,7 @@ namespace Perspex.Input if (this.Scope == scope) { - var interactive = this.Current as IInteractive; - - if (interactive != null) - { - interactive.RaiseEvent(new RoutedEventArgs - { - RoutedEvent = InputElement.LostFocusEvent, - }); - } - - this.Current = element; - KeyboardDevice.Instance.FocusedElement = element; - - interactive = element as IInteractive; - - if (interactive != null) - { - interactive.RaiseEvent(new GotFocusEventArgs - { - RoutedEvent = InputElement.GotFocusEvent, - KeyboardNavigated = keyboardNavigated, - }); - } + KeyboardDevice.Instance.SetFocusedElement(element, keyboardNavigated); } } diff --git a/Perspex.Input/IInputManager.cs b/Perspex.Input/IInputManager.cs index 53b6c0d96a..aafc4002fd 100644 --- a/Perspex.Input/IInputManager.cs +++ b/Perspex.Input/IInputManager.cs @@ -14,6 +14,8 @@ namespace Perspex.Input { IObservable RawEventReceived { get; } + IObservable PostProcess { get; } + void Process(RawInputEventArgs e); } } diff --git a/Perspex.Input/IKeyboardDevice.cs b/Perspex.Input/IKeyboardDevice.cs index a4801e55b7..a43fb0cd2a 100644 --- a/Perspex.Input/IKeyboardDevice.cs +++ b/Perspex.Input/IKeyboardDevice.cs @@ -28,8 +28,10 @@ namespace Perspex.Input public interface IKeyboardDevice : IInputDevice { - IInputElement FocusedElement { get; set; } + IInputElement FocusedElement { get; } ModifierKeys Modifiers { get; } + + void SetFocusedElement(IInputElement element, bool keyboardNavigated); } } diff --git a/Perspex.Input/IKeyboardNavigation.cs b/Perspex.Input/IKeyboardNavigation.cs index 0da52e8246..914ebd4c6f 100644 --- a/Perspex.Input/IKeyboardNavigation.cs +++ b/Perspex.Input/IKeyboardNavigation.cs @@ -1,5 +1,5 @@ // ----------------------------------------------------------------------- -// +// // Copyright 2014 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- @@ -8,8 +8,14 @@ namespace Perspex.Input { public interface IKeyboardNavigation { - bool MoveNext(IInputElement element); + IInputElement GetNextInTabOrder(IInputElement element); - bool MovePrevious(IInputElement element); + IInputElement GetPreviousInTabOrder(IInputElement element); + + void TabNext(IInputElement element); + + void TabPrevious(IInputElement element); + + void TabTo(IInputElement element); } } \ No newline at end of file diff --git a/Perspex.Input/InputElement.cs b/Perspex.Input/InputElement.cs index b364f443e2..6c5b3a7bb6 100644 --- a/Perspex.Input/InputElement.cs +++ b/Perspex.Input/InputElement.cs @@ -226,24 +226,20 @@ namespace Perspex.Input protected virtual void OnKeyDown(KeyEventArgs e) { - if (e.Key == Key.Tab) + if (e.Key == Key.Tab && !e.Handled) { - var modifiers = e.Device.Modifiers; + var shift = (e.Device.Modifiers & ModifierKeys.Shift) != 0; - if ((modifiers & ModifierKeys.Shift) == 0) + if (!shift) { - if (KeyboardNavigation.Instance.MoveNext(this)) - { - e.Handled = true; - } + KeyboardNavigation.Instance.TabNext(this); } else { - if (KeyboardNavigation.Instance.MovePrevious(this)) - { - e.Handled = true; - } + KeyboardNavigation.Instance.TabPrevious(this); } + + e.Handled = true; } } diff --git a/Perspex.Input/InputManager.cs b/Perspex.Input/InputManager.cs index c565735fdc..9989a2c05f 100644 --- a/Perspex.Input/InputManager.cs +++ b/Perspex.Input/InputManager.cs @@ -17,10 +17,18 @@ namespace Perspex.Input { private Subject rawEventReceived = new Subject(); + private Subject postProcess = new Subject(); + public static IInputManager Instance => Locator.Current.GetService(); public IObservable RawEventReceived => this.rawEventReceived; - public void Process(RawInputEventArgs e) => this.rawEventReceived.OnNext(e); + public IObservable PostProcess => this.postProcess; + + public void Process(RawInputEventArgs e) + { + this.rawEventReceived.OnNext(e); + this.postProcess.OnNext(e); + } } } diff --git a/Perspex.Input/KeyboardDevice.cs b/Perspex.Input/KeyboardDevice.cs index 9a7dea13c6..999db93ee5 100644 --- a/Perspex.Input/KeyboardDevice.cs +++ b/Perspex.Input/KeyboardDevice.cs @@ -10,6 +10,7 @@ namespace Perspex.Input using System.Linq; using System.Reactive.Linq; using Perspex.Input.Raw; + using Perspex.Interactivity; using Splat; public abstract class KeyboardDevice : IKeyboardDevice @@ -40,11 +41,36 @@ namespace Perspex.Input public IInputElement FocusedElement { get; - set; + private set; } public abstract ModifierKeys Modifiers { get; } + public void SetFocusedElement(IInputElement element, bool keyboardNavigated) + { + var interactive = this.FocusedElement as IInteractive; + + if (interactive != null) + { + interactive.RaiseEvent(new RoutedEventArgs + { + RoutedEvent = InputElement.LostFocusEvent, + }); + } + + this.FocusedElement = element; + interactive = element as IInteractive; + + if (interactive != null) + { + interactive.RaiseEvent(new GotFocusEventArgs + { + RoutedEvent = InputElement.GotFocusEvent, + KeyboardNavigated = keyboardNavigated, + }); + } + } + private void ProcessRawEvent(RawKeyEventArgs e) { IInputElement element = this.FocusedElement; diff --git a/Perspex.Input/KeyboardNavigation.cs b/Perspex.Input/KeyboardNavigation.cs index c9e5c15d44..e9b015b5c3 100644 --- a/Perspex.Input/KeyboardNavigation.cs +++ b/Perspex.Input/KeyboardNavigation.cs @@ -6,81 +6,289 @@ namespace Perspex.Input { + using System; + using System.Collections.Generic; using System.Linq; using Perspex.VisualTree; using Splat; public class KeyboardNavigation : IKeyboardNavigation { + public static readonly PerspexProperty TabNavigationProperty = + PerspexProperty.RegisterAttached("TabNavigation"); + + public static readonly PerspexProperty TabOnceActiveElementProperty = + PerspexProperty.RegisterAttached("TabOnceActiveElement"); + public static IKeyboardNavigation Instance { get { return Locator.Current.GetService(); } } - public bool MoveNext(IInputElement element) + public static KeyboardNavigationMode GetTabNavigation(InputElement element) { - var parent = element.GetVisualParent(); - var descendent = element.GetVisualDescendents() - .OfType() - .Where(x => x.Focusable && x.IsEnabledCore) - .FirstOrDefault(); + return element.GetValue(TabNavigationProperty); + } + + public static void SetTabNavigation(InputElement element, KeyboardNavigationMode value) + { + element.SetValue(TabNavigationProperty, value); + } + + public static IInputElement GetTabOnceActiveElement(InputElement element) + { + return element.GetValue(TabOnceActiveElementProperty); + } + + public static void SetTabOnceActiveElement(InputElement element, IInputElement value) + { + element.SetValue(TabOnceActiveElementProperty, value); + } + + public IInputElement GetNextInTabOrder(IInputElement element) + { + Contract.Requires(element != null); + + var container = element.GetVisualParent(); + + if (container != null) + { + var mode = GetTabNavigation((InputElement)container); + + switch (mode) + { + case KeyboardNavigationMode.Continue: + return GetNextInContainer(element, container) ?? + GetFirstInNextContainer(container); + case KeyboardNavigationMode.Cycle: + return GetNextInContainer(element, container) ?? + GetDescendents(container).FirstOrDefault(); + default: + return GetFirstInNextContainer(container); + } + } + else + { + return GetDescendents(element).FirstOrDefault(); + } + } + + public IInputElement GetPreviousInTabOrder(IInputElement element) + { + Contract.Requires(element != null); + + var container = element.GetVisualParent(); + + if (container != null) + { + var mode = GetTabNavigation((InputElement)container); + + switch (mode) + { + case KeyboardNavigationMode.Continue: + return GetPreviousInContainer(element, container) ?? + GetLastInPreviousContainer(element); + case KeyboardNavigationMode.Cycle: + return GetPreviousInContainer(element, container) ?? + GetDescendents(container).LastOrDefault(); + default: + return GetLastInPreviousContainer(container); + } + } + else + { + return GetDescendents(element).LastOrDefault(); + } + } + + public void TabNext(IInputElement element) + { + Contract.Requires(element != null); + + var next = GetNextInTabOrder(element); + + if (next != null) + { + TabTo(next); + } + } + + public void TabPrevious(IInputElement element) + { + Contract.Requires(element != null); + + var next = GetPreviousInTabOrder(element); + + if (next != null) + { + TabTo(next); + } + } + + public void TabTo(IInputElement element) + { + Contract.Requires(element != null); + + FocusManager.Instance.Focus(element, true); + } + + private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible; + + private static bool CanFocusDescendent(IInputElement e) => e.IsEnabledCore && e.IsVisible; + + private static IEnumerable GetDescendents(IInputElement element) + { + var mode = GetTabNavigation((InputElement)element); + + if (mode == KeyboardNavigationMode.Never) + { + yield break; + } + + var children = element.GetVisualChildren().OfType(); + + if (mode == KeyboardNavigationMode.Once) + { + var active = GetTabOnceActiveElement((InputElement)element); + + if (active != null) + { + yield return active; + yield break; + } + else + { + children = children.Take(1); + } + } + + foreach (var child in children) + { + if (CanFocus(child)) + { + yield return child; + } + + if (CanFocusDescendent(child)) + { + foreach (var descendent in GetDescendents(child)) + { + yield return descendent; + } + } + } + } + + private static IInputElement GetNextInContainer(IInputElement element, IInputElement container) + { + var descendent = GetDescendents(element).FirstOrDefault(); if (descendent != null) { - FocusManager.Instance.Focus(descendent, true); - return true; + return descendent; } - else if (parent != null) + else if (container != null) { - var sibling = parent.GetVisualChildren() + var sibling = container.GetVisualChildren() .OfType() - .Where(x => x.Focusable && x.IsEnabledCore) + .Where(CanFocus) .SkipWhile(x => x != element) .Skip(1) .FirstOrDefault(); if (sibling != null) { - FocusManager.Instance.Focus(sibling, true); - return true; + return sibling; } } - return false; + return null; } - public bool MovePrevious(IInputElement element) + private static IInputElement GetPreviousInContainer(IInputElement element, IInputElement container) { - var parent = element.GetVisualParent(); - var descendent = element.GetVisualDescendents() + return container.GetVisualChildren() .OfType() - .Where(x => x.Focusable && x.IsEnabledCore) - .Reverse() - .FirstOrDefault(); + .Where(CanFocus) + .TakeWhile(x => x != element) + .LastOrDefault(); + } - if (descendent != null) - { - FocusManager.Instance.Focus(descendent, true); - return true; - } - else if (parent != null) + private static IInputElement GetFirstInNextContainer(IInputElement container) + { + var parent = container.GetVisualParent(); + IInputElement next = null; + + if (parent != null) { - var previous = parent.GetVisualChildren() + var sibling = parent.GetVisualChildren() .OfType() - .Where(x => x.Focusable && x.IsEnabledCore) - .Reverse() - .SkipWhile(x => x != element) + .Where(CanFocusDescendent) + .SkipWhile(x => x != container) .Skip(1) .FirstOrDefault(); - if (previous != null) + if (sibling != null) + { + if (CanFocus(sibling)) + { + next = sibling; + } + else + { + next = GetDescendents(sibling).FirstOrDefault(); + } + } + + if (next == null) + { + next = GetFirstInNextContainer(parent); + } + } + else + { + next = GetDescendents(container).FirstOrDefault(); + } + + return next; + } + + private static IInputElement GetLastInPreviousContainer(IInputElement container) + { + var parent = container.GetVisualParent(); + IInputElement next = null; + + if (parent != null) + { + var sibling = parent.GetVisualChildren() + .OfType() + .Where(CanFocusDescendent) + .TakeWhile(x => x != container) + .LastOrDefault(); + + if (sibling != null) { - FocusManager.Instance.Focus(previous, true); - return true; + if (CanFocus(sibling)) + { + next = sibling; + } + else + { + next = GetDescendents(sibling).LastOrDefault(); + } } + + if (next == null) + { + next = GetLastInPreviousContainer(parent); + } + } + else + { + next = GetDescendents(container).LastOrDefault(); } - return false; + return next; } } } diff --git a/Perspex.Input/KeyboardNavigationMode.cs b/Perspex.Input/KeyboardNavigationMode.cs new file mode 100644 index 0000000000..346b4099e7 --- /dev/null +++ b/Perspex.Input/KeyboardNavigationMode.cs @@ -0,0 +1,16 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Input +{ + public enum KeyboardNavigationMode + { + Continue, + Cycle, + Once, + Never, + } +} \ No newline at end of file diff --git a/Perspex.Input/Perspex.Input.csproj b/Perspex.Input/Perspex.Input.csproj index 44a7ee1904..69fb1b83c0 100644 --- a/Perspex.Input/Perspex.Input.csproj +++ b/Perspex.Input/Perspex.Input.csproj @@ -81,6 +81,7 @@ + diff --git a/Perspex.sln b/Perspex.sln index 8e2bc34174..887761eb67 100644 --- a/Perspex.sln +++ b/Perspex.sln @@ -65,6 +65,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Direct2D1.RenderTes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Cairo.RenderTests", "Tests\Perspex.RenderTests\Perspex.Cairo.RenderTests.csproj", "{E106CF37-4066-4615-B684-172A6D30B058}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Input.UnitTests", "Tests\Perspex.Input.UnitTests\Perspex.Input.UnitTests.csproj", "{AC18926A-E784-40FE-B09D-BB0FE2B599F0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -175,6 +177,10 @@ Global {E106CF37-4066-4615-B684-172A6D30B058}.Debug|Any CPU.Build.0 = Debug|Any CPU {E106CF37-4066-4615-B684-172A6D30B058}.Release|Any CPU.ActiveCfg = Release|Any CPU {E106CF37-4066-4615-B684-172A6D30B058}.Release|Any CPU.Build.0 = Release|Any CPU + {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC18926A-E784-40FE-B09D-BB0FE2B599F0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -193,5 +199,6 @@ Global {08478EF5-44E8-42E9-92D6-15E00EC038D8} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {DABFD304-D6A4-4752-8123-C2CCF7AC7831} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {E106CF37-4066-4615-B684-172A6D30B058} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {AC18926A-E784-40FE-B09D-BB0FE2B599F0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection EndGlobal diff --git a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index 9b9234ef19..aeb260dab5 100644 --- a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -41,11 +41,13 @@ False ..\..\packages\Moq.4.2.1409.1722\lib\net40\Moq.dll - - ..\..\packages\AutoFixture.3.21.1\lib\net40\Ploeh.AutoFixture.dll + + ..\..\packages\AutoFixture.3.30.3\lib\net40\Ploeh.AutoFixture.dll + True - - ..\..\packages\AutoFixture.AutoMoq.3.21.1\lib\net40\Ploeh.AutoFixture.AutoMoq.dll + + ..\..\packages\AutoFixture.AutoMoq.3.30.3\lib\net40\Ploeh.AutoFixture.AutoMoq.dll + True False diff --git a/Tests/Perspex.Controls.UnitTests/packages.config b/Tests/Perspex.Controls.UnitTests/packages.config index 2fdc71c443..1d1786278c 100644 --- a/Tests/Perspex.Controls.UnitTests/packages.config +++ b/Tests/Perspex.Controls.UnitTests/packages.config @@ -1,18 +1,18 @@  - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/Perspex.Input.UnitTests/KeyboardNavigationTests.cs b/Tests/Perspex.Input.UnitTests/KeyboardNavigationTests.cs new file mode 100644 index 0000000000..0473ac70e3 --- /dev/null +++ b/Tests/Perspex.Input.UnitTests/KeyboardNavigationTests.cs @@ -0,0 +1,856 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Input.UnitTests +{ + using Perspex.Controls; + using Xunit; + + public class KeyboardNavigationTests + { + [Fact] + public void GetNextInTabOrder_Continue_Returns_Next_Control_In_Container() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + Children = new Controls + { + new Button { Id = "Button1" }, + (current = new Button { Id = "Button2" }), + (next = new Button { Id = "Button3" }), + } + }), + new StackPanel + { + Children = new Controls + { + new Button { Id = "Button4" }, + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetNextInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetNextInTabOrder_Continue_Returns_First_Control_In_Next_Sibling_Container() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + Children = new Controls + { + new Button { Id = "Button1" }, + new Button { Id = "Button2" }, + (current = new Button { Id = "Button3" }), + } + }), + new StackPanel + { + Children = new Controls + { + (next = new Button { Id = "Button4" }), + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetNextInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetNextInTabOrder_Continue_Returns_Next_Sibling() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + Children = new Controls + { + new Button { Id = "Button1" }, + new Button { Id = "Button2" }, + (current = new Button { Id = "Button3" }), + } + }), + (next = new Button { Id = "Button4" }), + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetNextInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetNextInTabOrder_Continue_Returns_First_Control_In_Next_Uncle_Container() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + Children = new Controls + { + new Button { Id = "Button1" }, + new Button { Id = "Button2" }, + (current = new Button { Id = "Button3" }), + } + }), + }, + }, + new StackPanel + { + Children = new Controls + { + (next = new Button { Id = "Button4" }), + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetNextInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetNextInTabOrder_Continue_Returns_Child_Of_Top_Level() + { + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (next = new Button { Id = "Button1" }), + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetNextInTabOrder(top); + + Assert.Equal(next, result); + } + + [Fact] + public void GetNextInTabOrder_Continue_Wraps() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + Children = new Controls + { + (next = new Button { Id = "Button1" }), + new Button { Id = "Button2" }, + new Button { Id = "Button3" }, + } + }), + }, + }, + new StackPanel + { + Children = new Controls + { + new Button { Id = "Button4" }, + new Button { Id = "Button5" }, + (current = new Button { Id = "Button6" }), + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetNextInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetNextInTabOrder_Cycle_Returns_Next_Control_In_Container() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, + Children = new Controls + { + new Button { Id = "Button1" }, + (current = new Button { Id = "Button2" }), + (next = new Button { Id = "Button3" }), + } + }), + new StackPanel + { + Children = new Controls + { + new Button { Id = "Button4" }, + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetNextInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetNextInTabOrder_Cycle_Wraps_To_First() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, + Children = new Controls + { + (next = new Button { Id = "Button1" }), + new Button { Id = "Button2" }, + (current = new Button { Id = "Button3" }), + } + }), + new StackPanel + { + Children = new Controls + { + new Button { Id = "Button4" }, + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetNextInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetNextInTabOrder_Once_Moves_To_Next_Container() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once, + Children = new Controls + { + new Button { Id = "Button1" }, + (current = new Button { Id = "Button2" }), + new Button { Id = "Button3" }, + } + }), + new StackPanel + { + Children = new Controls + { + (next = new Button { Id = "Button4" }), + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetNextInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetNextInTabOrder_Once_Moves_To_Active_Element() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once, + Children = new Controls + { + new Button { Id = "Button1" }, + (next = new Button { Id = "Button2" }), + new Button { Id = "Button3" }, + } + }), + new StackPanel + { + Children = new Controls + { + new Button { Id = "Button4" }, + new Button { Id = "Button5" }, + (current = new Button { Id = "Button6" }), + } + }, + } + }; + + KeyboardNavigation.SetTabOnceActiveElement(container, next); + + var target = new KeyboardNavigation(); + var result = target.GetNextInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetNextInTabOrder_Never_Moves_To_Next_Container() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Never, + Children = new Controls + { + new Button { Id = "Button1" }, + (current = new Button { Id = "Button2" }), + new Button { Id = "Button3" }, + } + }), + new StackPanel + { + Children = new Controls + { + (next = new Button { Id = "Button4" }), + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetNextInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetNextInTabOrder_Never_Skips_Container() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Never, + Children = new Controls + { + new Button { Id = "Button1" }, + new Button { Id = "Button2" }, + new Button { Id = "Button3" }, + } + }), + new StackPanel + { + Children = new Controls + { + (next = new Button { Id = "Button4" }), + new Button { Id = "Button5" }, + (current = new Button { Id = "Button6" }), + } + }, + } + }; + + KeyboardNavigation.SetTabOnceActiveElement(container, next); + + var target = new KeyboardNavigation(); + var result = target.GetNextInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetPreviousInTabOrder_Continue_Returns_Previous_Control_In_Container() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + Children = new Controls + { + new Button { Id = "Button1" }, + (next = new Button { Id = "Button2" }), + (current = new Button { Id = "Button3" }), + } + }), + new StackPanel + { + Children = new Controls + { + new Button { Id = "Button4" }, + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetPreviousInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetPreviousInTabOrder_Continue_Returns_Last_Control_In_Previous_Sibling_Container() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + Children = new Controls + { + new Button { Id = "Button1" }, + new Button { Id = "Button2" }, + (next = new Button { Id = "Button3" }), + } + }), + new StackPanel + { + Children = new Controls + { + (current = new Button { Id = "Button4" }), + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetPreviousInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetPreviousInTabOrder_Continue_Returns_Last_Child_Of_Sibling() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + Children = new Controls + { + new Button { Id = "Button1" }, + new Button { Id = "Button2" }, + (next = new Button { Id = "Button3" }), + } + }), + (current = new Button { Id = "Button4" }), + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetPreviousInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetPreviousInTabOrder_Continue_Returns_Last_Control_In_Previous_Nephew_Container() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + Children = new Controls + { + new Button { Id = "Button1" }, + new Button { Id = "Button2" }, + (next = new Button { Id = "Button3" }), + } + }), + }, + }, + new StackPanel + { + Children = new Controls + { + (current = new Button { Id = "Button4" }), + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetPreviousInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetPreviousInTabOrder_Continue_Wraps() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + Children = new Controls + { + (current = new Button { Id = "Button1" }), + new Button { Id = "Button2" }, + new Button { Id = "Button3" }, + } + }), + }, + }, + new StackPanel + { + Children = new Controls + { + new Button { Id = "Button4" }, + new Button { Id = "Button5" }, + (next = new Button { Id = "Button6" }), + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetPreviousInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetPreviousInTabOrder_Cycle_Returns_Previous_Control_In_Container() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, + Children = new Controls + { + (next = new Button { Id = "Button1" }), + (current = new Button { Id = "Button2" }), + new Button { Id = "Button3" }, + } + }), + new StackPanel + { + Children = new Controls + { + new Button { Id = "Button4" }, + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetPreviousInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetPreviousInTabOrder_Cycle_Wraps_To_Last() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, + Children = new Controls + { + (current = new Button { Id = "Button1" }), + new Button { Id = "Button2" }, + (next = new Button { Id = "Button3" }), + } + }), + new StackPanel + { + Children = new Controls + { + new Button { Id = "Button4" }, + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetPreviousInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetPreviousInTabOrder_Once_Moves_To_Previous_Container() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + Children = new Controls + { + new Button { Id = "Button1" }, + new Button { Id = "Button2" }, + (next = new Button { Id = "Button3" }), + } + }), + new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once, + Children = new Controls + { + new Button { Id = "Button4" }, + (current = new Button { Id = "Button5" }), + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetPreviousInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetPreviousInTabOrder_Once_Moves_To_Active_Element() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once, + Children = new Controls + { + new Button { Id = "Button1" }, + (next = new Button { Id = "Button2" }), + new Button { Id = "Button3" }, + } + }), + new StackPanel + { + Children = new Controls + { + (current = new Button { Id = "Button4" }), + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + KeyboardNavigation.SetTabOnceActiveElement(container, next); + + var target = new KeyboardNavigation(); + var result = target.GetPreviousInTabOrder(current); + + Assert.Equal(next, result); + } + + [Fact] + public void GetPreviousInTabOrder_Once_Moves_To_First_Element() + { + StackPanel container; + Button current; + Button next; + + var top = new StackPanel + { + Children = new Controls + { + (container = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once, + Children = new Controls + { + (next = new Button { Id = "Button1" }), + new Button { Id = "Button2" }, + new Button { Id = "Button3" }, + } + }), + new StackPanel + { + Children = new Controls + { + (current = new Button { Id = "Button4" }), + new Button { Id = "Button5" }, + new Button { Id = "Button6" }, + } + }, + } + }; + + var target = new KeyboardNavigation(); + var result = target.GetPreviousInTabOrder(current); + + Assert.Equal(next, result); + } + } +} diff --git a/Tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj b/Tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj new file mode 100644 index 0000000000..c785a2fa74 --- /dev/null +++ b/Tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj @@ -0,0 +1,134 @@ + + + + + + + + Debug + AnyCPU + {AC18926A-E784-40FE-B09D-BB0FE2B599F0} + Library + Properties + Perspex.Input.UnitTests + Perspex.Input.UnitTests + v4.5 + 512 + 90580c1a + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\Moq.4.2.1502.911\lib\net40\Moq.dll + True + + + ..\..\packages\AutoFixture.3.30.3\lib\net40\Ploeh.AutoFixture.dll + True + + + ..\..\packages\AutoFixture.AutoMoq.3.30.3\lib\net40\Ploeh.AutoFixture.AutoMoq.dll + True + + + ..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll + True + + + + + + + + + + ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True + + + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True + + + ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True + + + + + + + + + + + + + {d211e587-d8bc-45b9-95a4-f297c8fa5200} + Perspex.Animation + + + {b09b78d8-9b26-48b0-9149-d64a2f120f3f} + Perspex.Base + + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Perspex.Controls + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Perspex.Input + + + {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} + Perspex.Interactivity + + + {42472427-4774-4c81-8aff-9f27b8e31721} + Perspex.Layout + + + {eb582467-6abb-43a1-b052-e981ba910e3a} + Perspex.SceneGraph + + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Perspex.Styling + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + \ No newline at end of file diff --git a/Tests/Perspex.Input.UnitTests/Properties/AssemblyInfo.cs b/Tests/Perspex.Input.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..0b681f4272 --- /dev/null +++ b/Tests/Perspex.Input.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Perspex.Input.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Perspex.Input.UnitTests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ac18926a-e784-40fe-b09d-bb0fe2b599f0")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Tests/Perspex.Input.UnitTests/app.config b/Tests/Perspex.Input.UnitTests/app.config new file mode 100644 index 0000000000..133577da84 --- /dev/null +++ b/Tests/Perspex.Input.UnitTests/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/Perspex.Input.UnitTests/packages.config b/Tests/Perspex.Input.UnitTests/packages.config new file mode 100644 index 0000000000..d949519049 --- /dev/null +++ b/Tests/Perspex.Input.UnitTests/packages.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Windows/Perspex.Win32/Input/WindowsKeyboardDevice.cs b/Windows/Perspex.Win32/Input/WindowsKeyboardDevice.cs index 998ad78ec2..673e0d71c2 100644 --- a/Windows/Perspex.Win32/Input/WindowsKeyboardDevice.cs +++ b/Windows/Perspex.Win32/Input/WindowsKeyboardDevice.cs @@ -54,7 +54,7 @@ namespace Perspex.Win32.Input public void WindowActivated(Window window) { - this.FocusedElement = window; + this.SetFocusedElement(window, false); } public string StringFromVirtualKey(uint virtualKey)