From 2584f4bb18ebacbf66cc1d4e3e387479dec27563 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 22 Jul 2018 22:29:00 +0200 Subject: [PATCH 01/23] Removed directional navigation code. --- src/Avalonia.Controls/MenuItem.cs | 5 +- .../Presenters/ItemsPresenter.cs | 9 +- src/Avalonia.Controls/TreeViewItem.cs | 5 +- src/Avalonia.Input/KeyboardNavigation.cs | 33 - .../KeyboardNavigationHandler.cs | 47 +- .../Navigation/DirectionalNavigation.cs | 242 ------ .../KeyboardNavigationTests_Arrows.cs | 799 ------------------ .../KeyboardNavigationTests_Custom.cs | 31 - 8 files changed, 9 insertions(+), 1162 deletions(-) delete mode 100644 src/Avalonia.Input/Navigation/DirectionalNavigation.cs delete mode 100644 tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 96f6fb59b0..cec653e045 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -72,10 +72,7 @@ namespace Avalonia.Controls /// The default value for the property. /// private static readonly ITemplate DefaultPanel = - new FuncTemplate(() => new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - }); + new FuncTemplate(() => new StackPanel()); /// /// The timer used to display submenus. diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index f8d62a1cbf..500c7aa187 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -143,13 +143,6 @@ namespace Avalonia.Controls.Presenters Virtualizer = ItemVirtualizer.Create(this); ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); - if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) - { - KeyboardNavigation.SetDirectionalNavigation( - (InputElement)Panel, - KeyboardNavigationMode.Contained); - } - KeyboardNavigation.SetTabNavigation( (InputElement)Panel, KeyboardNavigation.GetTabNavigation(this)); @@ -175,4 +168,4 @@ namespace Avalonia.Controls.Presenters ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index bed27ef033..8af3333dd4 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -32,10 +32,7 @@ namespace Avalonia.Controls ListBoxItem.IsSelectedProperty.AddOwner(); private static readonly ITemplate DefaultPanel = - new FuncTemplate(() => new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - }); + new FuncTemplate(() => new StackPanel()); private TreeView _treeView; private bool _isExpanded; diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs index cbd9a74f4c..0277876e24 100644 --- a/src/Avalonia.Input/KeyboardNavigation.cs +++ b/src/Avalonia.Input/KeyboardNavigation.cs @@ -8,19 +8,6 @@ namespace Avalonia.Input /// public static class KeyboardNavigation { - /// - /// Defines the DirectionalNavigation attached property. - /// - /// - /// The DirectionalNavigation attached property defines how pressing arrow keys causes - /// focus to be navigated between the children of the container. - /// - public static readonly AttachedProperty DirectionalNavigationProperty = - AvaloniaProperty.RegisterAttached( - "DirectionalNavigation", - typeof(KeyboardNavigation), - KeyboardNavigationMode.None); - /// /// Defines the TabNavigation attached property. /// @@ -46,26 +33,6 @@ namespace Avalonia.Input "TabOnceActiveElement", typeof(KeyboardNavigation)); - /// - /// Gets the for a container. - /// - /// The container. - /// The for the container. - public static KeyboardNavigationMode GetDirectionalNavigation(InputElement element) - { - return element.GetValue(DirectionalNavigationProperty); - } - - /// - /// Sets the for a container. - /// - /// The container. - /// The for the container. - public static void SetDirectionalNavigation(InputElement element, KeyboardNavigationMode value) - { - element.SetValue(DirectionalNavigationProperty, value); - } - /// /// Gets the for a container. /// diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index bf2b61d08b..bc3098a7fb 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -85,7 +85,7 @@ namespace Avalonia.Input } else { - return DirectionalNavigation.GetNext(element, direction); + throw new NotSupportedException(); } } @@ -122,47 +122,12 @@ namespace Avalonia.Input { var current = FocusManager.Instance.Current; - if (current != null) + if (current != null && e.Key == Key.Tab) { - NavigationDirection? direction = null; - - switch (e.Key) - { - case Key.Tab: - direction = (e.Modifiers & InputModifiers.Shift) == 0 ? - NavigationDirection.Next : NavigationDirection.Previous; - break; - 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.PageUp: - direction = NavigationDirection.PageUp; - break; - case Key.PageDown: - direction = NavigationDirection.PageDown; - break; - case Key.Home: - direction = NavigationDirection.First; - break; - case Key.End: - direction = NavigationDirection.Last; - break; - } - - if (direction.HasValue) - { - Move(current, direction.Value, e.Modifiers); - e.Handled = true; - } + var direction = (e.Modifiers & InputModifiers.Shift) == 0 ? + NavigationDirection.Next : NavigationDirection.Previous; + Move(current, direction, e.Modifiers); + e.Handled = true; } } } diff --git a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs deleted file mode 100644 index 75cb3a39e8..0000000000 --- a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.VisualTree; - -namespace Avalonia.Input.Navigation -{ - /// - /// The implementation for default directional navigation. - /// - public static class DirectionalNavigation - { - /// - /// Gets the next control in the specified navigation direction. - /// - /// The element. - /// The navigation direction. - /// - /// The next element in the specified direction, or null if - /// was the last in the requested direction. - /// - public static IInputElement GetNext( - IInputElement element, - NavigationDirection direction) - { - Contract.Requires(element != null); - Contract.Requires( - direction != NavigationDirection.Next && - direction != NavigationDirection.Previous); - - var container = element.GetVisualParent(); - - if (container != null) - { - var mode = KeyboardNavigation.GetDirectionalNavigation((InputElement)container); - - switch (mode) - { - case KeyboardNavigationMode.Continue: - return GetNextInContainer(element, container, direction) ?? - GetFirstInNextContainer(element, element, direction); - case KeyboardNavigationMode.Cycle: - return GetNextInContainer(element, container, direction) ?? - GetFocusableDescendant(container, direction); - case KeyboardNavigationMode.Contained: - return GetNextInContainer(element, container, direction); - default: - return null; - } - } - else - { - return GetFocusableDescendants(element).FirstOrDefault(); - } - } - - /// - /// Returns a value indicting whether the specified direction is forward. - /// - /// The direction. - /// True if the direction is forward. - private static bool IsForward(NavigationDirection direction) - { - return direction == NavigationDirection.Next || - direction == NavigationDirection.Last || - direction == NavigationDirection.Right || - direction == NavigationDirection.Down; - } - - /// - /// Gets the first or last focusable descendant of the specified element. - /// - /// The element. - /// The direction to search. - /// The element or null if not found.## - private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction) - { - return IsForward(direction) ? - GetFocusableDescendants(container).FirstOrDefault() : - GetFocusableDescendants(container).LastOrDefault(); - } - - /// - /// Gets the focusable descendants of the specified element. - /// - /// The element. - /// The element's focusable descendants. - private static IEnumerable GetFocusableDescendants(IInputElement element) - { - var children = element.GetVisualChildren().OfType(); - - foreach (var child in children) - { - if (child.CanFocus()) - { - yield return child; - } - - if (child.CanFocusDescendants()) - { - foreach (var descendant in GetFocusableDescendants(child)) - { - yield return descendant; - } - } - } - } - - /// - /// Gets the next item that should be focused in the specified container. - /// - /// The starting element/ - /// The container. - /// The direction. - /// The next element, or null if the element is the last. - private static IInputElement GetNextInContainer( - IInputElement element, - IInputElement container, - NavigationDirection direction) - { - if (direction == NavigationDirection.Down) - { - var descendant = GetFocusableDescendants(element).FirstOrDefault(); - - if (descendant != null) - { - return descendant; - } - } - - if (container != null) - { - var navigable = container as INavigableContainer; - - if (navigable != null) - { - while (element != null) - { - element = navigable.GetControl(direction, element); - - if (element != null && element.CanFocus()) - { - break; - } - } - } - else - { - // TODO: Do a spatial search here if the container doesn't implement - // INavigableContainer. - element = null; - } - - if (element != null && direction == NavigationDirection.Up) - { - var descendant = GetFocusableDescendants(element).LastOrDefault(); - - if (descendant != null) - { - return descendant; - } - } - - return element; - } - - return null; - } - - /// - /// Gets the first item that should be focused in the next container. - /// - /// The element being navigated away from. - /// The container. - /// The direction of the search. - /// The first element, or null if there are no more elements. - private static IInputElement GetFirstInNextContainer( - IInputElement element, - IInputElement container, - NavigationDirection direction) - { - var parent = container.GetVisualParent(); - var isForward = IsForward(direction); - IInputElement next = null; - - if (parent != null) - { - if (!isForward && parent.CanFocus()) - { - return parent; - } - - var siblings = parent.GetVisualChildren() - .OfType() - .Where(FocusExtensions.CanFocusDescendants); - var sibling = isForward ? - siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault() : - siblings.TakeWhile(x => x != container).LastOrDefault(); - - if (sibling != null) - { - if (sibling is ICustomKeyboardNavigation custom) - { - var (handled, customNext) = custom.GetNext(element, direction); - - if (handled) - { - return customNext; - } - } - - if (sibling.CanFocus()) - { - next = sibling; - } - else - { - next = isForward ? - GetFocusableDescendants(sibling).FirstOrDefault() : - GetFocusableDescendants(sibling).LastOrDefault(); - } - } - - if (next == null) - { - next = GetFirstInNextContainer(element, parent, direction); - } - } - else - { - next = isForward ? - GetFocusableDescendants(container).FirstOrDefault() : - GetFocusableDescendants(container).LastOrDefault(); - } - - return next; - } - } -} diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs deleted file mode 100644 index b81b724e2a..0000000000 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs +++ /dev/null @@ -1,799 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.Controls; -using Xunit; - -namespace Avalonia.Input.UnitTests -{ - public class KeyboardNavigationTests_Arrows - { - [Fact] - public void Down_Continue_Returns_Down_Control_In_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - (current = new Button { Name = "Button2" }), - (next = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Continue_Returns_First_Control_In_Down_Sibling_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (current = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (next = new Button { Name = "Button4" }), - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Continue_Returns_Down_Sibling() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (current = new Button { Name = "Button3" }), - } - }, - (next = new Button { Name = "Button4" }), - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Continue_Returns_First_Control_In_Down_Uncle_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (current = new Button { Name = "Button3" }), - } - }, - }, - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (next = new Button { Name = "Button4" }), - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Continue_Returns_Child_Of_Top_Level() - { - Button next; - - var top = new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (next = new Button { Name = "Button1" }), - } - }; - - var result = KeyboardNavigationHandler.GetNext(top, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Continue_Wraps() - { - Button current; - Button next; - - var top = new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (next = new Button { Name = "Button1" }), - new Button { Name = "Button2" }, - new Button { Name = "Button3" }, - } - }, - }, - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - (current = new Button { Name = "Button6" }), - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Cycle_Returns_Down_Control_In_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = - { - new Button { Name = "Button1" }, - (current = new Button { Name = "Button2" }), - (next = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Cycle_Wraps_To_First() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = - { - (next = new Button { Name = "Button1" }), - new Button { Name = "Button2" }, - (current = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Contained_Returns_Down_Control_In_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button1" }, - (current = new Button { Name = "Button2" }), - (next = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Equal(next, result); - } - - [Fact] - public void Down_Contained_Stops_At_End() - { - Button current; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (current = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Null(result); - } - - [Fact] - public void Down_None_Does_Nothing() - { - Button current; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.None, - Children = - { - new Button { Name = "Button1" }, - (current = new Button { Name = "Button2" }), - new Button { Name = "Button3" }, - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); - - Assert.Null(result); - } - - [Fact] - public void Up_Continue_Returns_Up_Control_In_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - (next = new Button { Name = "Button2" }), - (current = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Continue_Returns_Last_Control_In_Up_Sibling_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (next = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (current = new Button { Name = "Button4" }), - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Continue_Returns_Last_Child_Of_Sibling() - { - Button current; - Button next; - - var top = new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (next = new Button { Name = "Button3" }), - } - }, - (current = new Button { Name = "Button4" }), - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Continue_Returns_Last_Control_In_Up_Nephew_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button1" }, - new Button { Name = "Button2" }, - (next = new Button { Name = "Button3" }), - } - }, - }, - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (current = new Button { Name = "Button4" }), - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Continue_Wraps() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - (current = new Button { Name = "Button1" }), - new Button { Name = "Button2" }, - new Button { Name = "Button3" }, - } - }, - }, - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - (next = new Button { Name = "Button6" }), - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Continue_Returns_Parent() - { - Button current; - - var top = new Decorator - { - Focusable = true, - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Child = current = new Button - { - Name = "Button", - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(top, result); - } - - [Fact] - public void Up_Cycle_Returns_Up_Control_In_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = - { - (next = new Button { Name = "Button1" }), - (current = new Button { Name = "Button2" }), - new Button { Name = "Button3" }, - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Cycle_Wraps_To_Last() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = - { - (current = new Button { Name = "Button1" }), - new Button { Name = "Button2" }, - (next = new Button { Name = "Button3" }), - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Contained_Returns_Up_Control_In_Container() - { - Button current; - Button next; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - (next = new Button { Name = "Button1" }), - (current = new Button { Name = "Button2" }), - new Button { Name = "Button3" }, - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Equal(next, result); - } - - [Fact] - public void Up_Contained_Stops_At_Beginning() - { - Button current; - - var top = new StackPanel - { - Children = - { - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - (current = new Button { Name = "Button1" }), - new Button { Name = "Button2" }, - new Button { Name = "Button3" }, - } - }, - new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - new Button { Name = "Button4" }, - new Button { Name = "Button5" }, - new Button { Name = "Button6" }, - } - }, - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Null(result); - } - - [Fact] - public void Up_Contained_Doesnt_Return_Child_Control() - { - Decorator current; - - var top = new StackPanel - { - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = - { - (current = new Decorator - { - Focusable = true, - Child = new Button(), - }) - } - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); - - Assert.Null(result); - } - } -} diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs index a090dcd18d..ab0f5e2155 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs @@ -140,37 +140,6 @@ namespace Avalonia.Input.UnitTests Assert.Same(next, result); } - [Fact] - public void Right_Should_Custom_Navigate_From_Outside() - { - Button current; - Button next; - var target = new CustomNavigatingStackPanel - { - Children = - { - new Button { Content = "Button 1" }, - new Button { Content = "Button 2" }, - (next = new Button { Content = "Button 3" }), - }, - NextControl = next, - }; - - var root = new StackPanel - { - Children = - { - (current = new Button { Content = "Outside" }), - target, - }, - [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - }; - - var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Right); - - Assert.Same(next, result); - } - [Fact] public void Tab_Should_Navigate_Outside_When_Null_Returned_As_Next() { From 10c2ec64add55f7bceeec68d6140569a781f86ab Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 22 Jul 2018 22:53:40 +0200 Subject: [PATCH 02/23] Implement directional navigation in ItemsControl. --- src/Avalonia.Controls/ItemsControl.cs | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 3cb997f615..998111e8b9 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -323,6 +323,41 @@ namespace Avalonia.Controls LogicalChildren.RemoveAll(toRemove); } + protected override void OnKeyDown(KeyEventArgs e) + { + if (Presenter?.Panel is INavigableContainer container) + { + var focus = FocusManager.Instance; + var current = focus.Current; + NavigationDirection? direction = null; + + 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; + } + + if (direction != null && current != null) + { + var next = container.GetControl(direction.Value, current); + + if (next != null) + { + focus.Focus(next, NavigationMethod.Directional); + e.Handled = true; + } + } + } + + base.OnKeyDown(e); + } + /// /// Caled when the property changes. /// From 9e2e266d3c3a85c079e11137482b8c717884c699 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 Jul 2018 09:54:00 +0200 Subject: [PATCH 03/23] 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; + } + } + } } From 1e12b2c37b07cc3e046cb200aa8882f0f75a4a47 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Aug 2018 00:29:13 +0200 Subject: [PATCH 04/23] Don't focus non-focusable controls. When navigating `ItemsControl` with keyboard. --- src/Avalonia.Controls/ItemsControl.cs | 67 +++++++++++------ .../ItemsControlTests.cs | 72 +++++++++++++++++++ 2 files changed, 118 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index e2d2f0a516..7fdb75a9fc 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -330,31 +330,34 @@ namespace Avalonia.Controls /// The key events. protected override void OnKeyDown(KeyEventArgs e) { - 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()) + if (!e.Handled) { - return; - } - - var current = focus.Current - .GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(x => x.VisualParent == 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()) + { + return; + } - if (current != null) - { - var next = container.GetControl(direction.Value, current); + var current = focus.Current + .GetSelfAndVisualAncestors() + .OfType() + .FirstOrDefault(x => x.VisualParent == container); - if (next != null) + if (current != null) { - focus.Focus(next, NavigationMethod.Directional); - e.Handled = true; + var next = GetNextControl(container, direction.Value, current); + + if (next != null) + { + focus.Focus(next, NavigationMethod.Directional); + e.Handled = true; + } } } @@ -498,5 +501,27 @@ namespace Avalonia.Controls ItemCount = Items.Count(); } } + + protected static IInputElement GetNextControl( + INavigableContainer container, + NavigationDirection direction, + IInputElement from) + { + IInputElement result; + + while (from != null) + { + result = container.GetControl(direction, from); + + if (result?.Focusable == true) + { + return result; + } + + from = result; + } + + return null; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 9ef1e9f0d2..3cf886ade4 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -11,6 +11,7 @@ using Avalonia.VisualTree; using Xunit; using System.Collections.ObjectModel; using Avalonia.UnitTests; +using Avalonia.Input; namespace Avalonia.Controls.UnitTests { @@ -494,6 +495,77 @@ namespace Avalonia.Controls.UnitTests Assert.NotNull(NameScope.GetNameScope((TextBlock)container.Child)); } + [Fact] + public void Focuses_Next_Item_On_Key_Down() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var items = new object[] + { + new Button(), + new Button(), + }; + + var target = new ItemsControl + { + Template = GetTemplate(), + Items = items, + }; + + var root = new TestRoot { Child = target }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.Presenter.Panel.Children[0].Focus(); + + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Down, + }); + + Assert.Equal( + target.Presenter.Panel.Children[1], + FocusManager.Instance.Current); + } + } + + [Fact] + public void Does_Not_Focus_Non_Focusable_Item_On_Key_Down() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var items = new object[] + { + new Button(), + new Button { Focusable = false }, + new Button(), + }; + + var target = new ItemsControl + { + Template = GetTemplate(), + Items = items, + }; + + var root = new TestRoot { Child = target }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.Presenter.Panel.Children[0].Focus(); + + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Down, + }); + + Assert.Equal( + target.Presenter.Panel.Children[2], + FocusManager.Instance.Current); + } + } + private class Item { public Item(string value) From 2cf046e3ff7dacff95d80c71fb7cd535acda00c2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Aug 2018 00:30:19 +0200 Subject: [PATCH 05/23] Update menu item default themes. --- src/Avalonia.Themes.Default/MenuItem.xaml | 2 +- src/Avalonia.Themes.Default/Separator.xaml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 53965db016..5404117363 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -139,4 +139,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/Separator.xaml b/src/Avalonia.Themes.Default/Separator.xaml index 3b3a9e9749..6312a14df5 100644 --- a/src/Avalonia.Themes.Default/Separator.xaml +++ b/src/Avalonia.Themes.Default/Separator.xaml @@ -1,6 +1,7 @@ - \ No newline at end of file + From 1293e9af8df8d6309104ec97184ec40d39990975 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 14 Aug 2018 12:49:33 +0200 Subject: [PATCH 06/23] Implemented Menu interactions. --- src/Avalonia.Controls/Canvas.cs | 3 +- src/Avalonia.Controls/IMenu.cs | 21 + src/Avalonia.Controls/IMenuElement.cs | 40 ++ src/Avalonia.Controls/IMenuItem.cs | 41 ++ src/Avalonia.Controls/ItemsControl.cs | 11 +- src/Avalonia.Controls/Menu.cs | 250 ++++----- src/Avalonia.Controls/MenuItem.cs | 292 +++++------ .../Platform/DefaultMenuInteractionHandler.cs | 457 ++++++++++++++++ .../Platform/IMenuInteractionHandler.cs | 22 + .../Primitives/SelectingItemsControl.cs | 36 ++ src/Avalonia.Controls/StackPanel.cs | 58 ++- src/Avalonia.Controls/WrapPanel.cs | 5 +- src/Avalonia.Input/AccessKeyHandler.cs | 37 +- src/Avalonia.Input/IInputElement.cs | 2 +- src/Avalonia.Input/IMainMenu.cs | 7 + src/Avalonia.Input/INavigableContainer.cs | 3 +- src/Avalonia.Input/InputElement.cs | 2 +- .../Navigation/TabNavigation.cs | 2 +- src/Avalonia.Themes.Default/MenuItem.xaml | 5 - .../DefaultMenuInteractionHandlerTests.cs | 489 ++++++++++++++++++ .../VirtualizingStackPanelTests.cs | 2 +- 21 files changed, 1447 insertions(+), 338 deletions(-) create mode 100644 src/Avalonia.Controls/IMenu.cs create mode 100644 src/Avalonia.Controls/IMenuElement.cs create mode 100644 src/Avalonia.Controls/IMenuItem.cs create mode 100644 src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs create mode 100644 src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs create mode 100644 tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs diff --git a/src/Avalonia.Controls/Canvas.cs b/src/Avalonia.Controls/Canvas.cs index 8a80d6bdf7..5c9a97cb27 100644 --- a/src/Avalonia.Controls/Canvas.cs +++ b/src/Avalonia.Controls/Canvas.cs @@ -136,8 +136,9 @@ namespace Avalonia.Controls /// /// The movement direction. /// The control from which movement begins. + /// Whether to wrap around when the first or last item is reached. /// The control. - IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from, bool wrap) { // TODO: Implement this return null; diff --git a/src/Avalonia.Controls/IMenu.cs b/src/Avalonia.Controls/IMenu.cs new file mode 100644 index 0000000000..e118ec043c --- /dev/null +++ b/src/Avalonia.Controls/IMenu.cs @@ -0,0 +1,21 @@ +using System; +using Avalonia.Controls.Platform; + +namespace Avalonia.Controls +{ + /// + /// Represents a or . + /// + public interface IMenu : IMenuElement + { + /// + /// Gets the menu interaction handler. + /// + IMenuInteractionHandler InteractionHandler { get; } + + /// + /// Gets a value indicating whether the menu is open. + /// + bool IsOpen { get; } + } +} diff --git a/src/Avalonia.Controls/IMenuElement.cs b/src/Avalonia.Controls/IMenuElement.cs new file mode 100644 index 0000000000..c9fc04dcc8 --- /dev/null +++ b/src/Avalonia.Controls/IMenuElement.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using Avalonia.Input; + +namespace Avalonia.Controls +{ + /// + /// Represents an or . + /// + public interface IMenuElement : IControl + { + /// + /// Gets or sets the currently selected submenu item. + /// + IMenuItem SelectedItem { get; set; } + + /// + /// Gets the submenu items. + /// + IEnumerable SubItems { get; } + + /// + /// Opens the menu or menu item. + /// + void Open(); + + /// + /// Closes the menu or menu item. + /// + void Close(); + + /// + /// Moves the submenu selection in the specified direction. + /// + /// The direction. + /// Whether to wrap after the first or last item. + /// True if the selection was moved; otherwise false. + bool MoveSelection(NavigationDirection direction, bool wrap); + } +} diff --git a/src/Avalonia.Controls/IMenuItem.cs b/src/Avalonia.Controls/IMenuItem.cs new file mode 100644 index 0000000000..2657b1949f --- /dev/null +++ b/src/Avalonia.Controls/IMenuItem.cs @@ -0,0 +1,41 @@ +using System; + +namespace Avalonia.Controls +{ + /// + /// Represents a . + /// + public interface IMenuItem : IMenuElement + { + /// + /// Gets or sets a value that indicates whether the item has a submenu. + /// + bool HasSubMenu { get; } + + /// + /// Gets a value indicating whether the mouse is currently over the menu item's submenu. + /// + bool IsPointerOverSubMenu { get; } + + /// + /// Gets or sets a value that indicates whether the submenu of the is + /// open. + /// + bool IsSubMenuOpen { get; set; } + + /// + /// Gets a value that indicates whether the is a top-level main menu item. + /// + bool IsTopLevel { get; } + + /// + /// Gets the parent . + /// + new IMenuElement Parent { get; } + + /// + /// Raises a click event on the menu item. + /// + void RaiseClick(); + } +} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 7fdb75a9fc..676e0af3de 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -351,7 +351,7 @@ namespace Avalonia.Controls if (current != null) { - var next = GetNextControl(container, direction.Value, current); + var next = GetNextControl(container, direction.Value, current, false); if (next != null) { @@ -505,13 +505,14 @@ namespace Avalonia.Controls protected static IInputElement GetNextControl( INavigableContainer container, NavigationDirection direction, - IInputElement from) + IInputElement from, + bool wrap) { IInputElement result; - while (from != null) + do { - result = container.GetControl(direction, from); + result = container.GetControl(direction, from, wrap); if (result?.Focusable == true) { @@ -519,7 +520,7 @@ namespace Avalonia.Controls } from = result; - } + } while (from != null); return null; } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 994af9dab8..edd7ed489e 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -2,30 +2,23 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; using Avalonia.Controls.Generators; +using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; -using Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Rendering; namespace Avalonia.Controls { /// /// A top-level menu control. /// - public class Menu : SelectingItemsControl, IFocusScope, IMainMenu + public class Menu : SelectingItemsControl, IFocusScope, IMainMenu, IMenu { - /// - /// Defines the default items panel used by a . - /// - private static readonly ITemplate DefaultPanel = - new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); - /// /// Defines the property. /// @@ -34,12 +27,42 @@ namespace Avalonia.Controls nameof(IsOpen), o => o.IsOpen); + /// + /// Defines the event. + /// + public static readonly RoutedEvent MenuOpenedEvent = + RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent MenuClosedEvent = + RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); + + private static readonly ITemplate DefaultPanel = + new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); + private readonly IMenuInteractionHandler _interaction; private bool _isOpen; /// - /// Tracks event handlers added to the root of the visual tree. + /// Initializes a new instance of the class. + /// + public Menu() + { + _interaction = AvaloniaLocator.Current.GetService() ?? + new DefaultMenuInteractionHandler(); + } + + /// + /// Initializes a new instance of the class. /// - private IDisposable _subscription; + /// The menu iteraction handler. + public Menu(IMenuInteractionHandler interactionHandler) + { + Contract.Requires(interactionHandler != null); + + _interaction = interactionHandler; + } /// /// Initializes static members of the class. @@ -47,7 +70,6 @@ namespace Avalonia.Controls static Menu() { ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); - MenuItem.ClickEvent.AddClassHandler(x => x.OnMenuClick, handledEventsToo: true); MenuItem.SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); } @@ -60,18 +82,52 @@ namespace Avalonia.Controls private set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } } - /// - /// Gets the selected container. - /// - private MenuItem SelectedMenuItem + /// + IMenuInteractionHandler IMenu.InteractionHandler => _interaction; + + /// + IMenuItem IMenuElement.SelectedItem { get { var index = SelectedIndex; return (index != -1) ? - (MenuItem)ItemContainerGenerator.ContainerFromIndex(index) : + (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : null; } + set + { + SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); + } + } + + /// + IEnumerable IMenuElement.SubItems + { + get + { + return ItemContainerGenerator.Containers + .Select(x => x.ContainerControl) + .OfType(); + } + } + + /// + /// Occurs when a is opened. + /// + public event EventHandler MenuOpened + { + add { AddHandler(MenuOpenedEvent, value); } + remove { RemoveHandler(MenuOpenedEvent, value); } + } + + /// + /// Occurs when a is closed. + /// + public event EventHandler MenuClosed + { + add { AddHandler(MenuClosedEvent, value); } + remove { RemoveHandler(MenuClosedEvent, value); } } /// @@ -79,13 +135,22 @@ namespace Avalonia.Controls /// public void Close() { - foreach (MenuItem i in this.GetLogicalChildren()) + if (IsOpen) { - i.IsSubMenuOpen = false; - } + foreach (var i in ((IMenu)this).SubItems) + { + i.Close(); + } + + IsOpen = false; + SelectedIndex = -1; - IsOpen = false; - SelectedIndex = -1; + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); + } } /// @@ -93,9 +158,25 @@ namespace Avalonia.Controls /// public void Open() { - SelectedIndex = 0; - SelectedMenuItem.Focus(); - IsOpen = true; + if (!IsOpen) + { + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); + } + } + + /// + bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); + + /// + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); } /// @@ -103,79 +184,27 @@ namespace Avalonia.Controls { base.OnAttachedToVisualTree(e); - var topLevel = (TopLevel)e.Root; - var window = e.Root as Window; - - if (window != null) - window.Deactivated += Deactivated; - - var pointerPress = topLevel.AddHandler( - PointerPressedEvent, - TopLevelPreviewPointerPress, - RoutingStrategies.Tunnel); - - _subscription = new CompositeDisposable( - pointerPress, - Disposable.Create(() => - { - if (window != null) - window.Deactivated -= Deactivated; - }), - InputManager.Instance.Process.Subscribe(ListenForNonClientClick)); - var inputRoot = e.Root as IInputRoot; if (inputRoot?.AccessKeyHandler != null) { inputRoot.AccessKeyHandler.MainMenu = this; } + + _interaction.Attach(this); } /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - _subscription.Dispose(); + _interaction.Detach(this); } /// - protected override IItemContainerGenerator CreateItemContainerGenerator() - { - return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); - } - - /// - /// Called when a key is pressed within the menu. - /// - /// The event args. protected override void OnKeyDown(KeyEventArgs e) { - bool menuWasOpen = SelectedMenuItem?.IsSubMenuOpen ?? false; - - base.OnKeyDown(e); - - if (menuWasOpen) - { - // If a menu item was open and we navigate to a new one with the arrow keys, open - // that menu and select the first item. - var selection = SelectedMenuItem; - - if (selection != null && !selection.IsSubMenuOpen) - { - selection.IsSubMenuOpen = true; - selection.SelectedIndex = 0; - } - } - } - - /// - /// Called when the menu loses focus. - /// - /// The event args. - protected override void OnLostFocus(RoutedEventArgs e) - { - base.OnLostFocus(e); - SelectedItem = null; + // Don't handle here: let the interaction handler handle it. } /// @@ -184,9 +213,7 @@ namespace Avalonia.Controls /// The event args. protected virtual void OnSubmenuOpened(RoutedEventArgs e) { - var menuItem = e.Source as MenuItem; - - if (menuItem != null && menuItem.Parent == this) + if (e.Source is MenuItem menuItem && menuItem.Parent == this) { foreach (var child in this.GetLogicalChildren().OfType()) { @@ -199,58 +226,5 @@ namespace Avalonia.Controls IsOpen = true; } - - /// - /// Called when the top-level window is deactivated. - /// - /// The sender. - /// The event args. - private void Deactivated(object sender, EventArgs e) - { - Close(); - } - - /// - /// Listens for non-client clicks and closes the menu when one is detected. - /// - /// The raw event. - private void ListenForNonClientClick(RawInputEventArgs e) - { - var mouse = e as RawMouseEventArgs; - - if (mouse?.Type == RawMouseEventType.NonClientLeftButtonDown) - { - Close(); - } - } - - /// - /// Called when a submenu is clicked somewhere in the menu. - /// - /// The event args. - private void OnMenuClick(RoutedEventArgs e) - { - Close(); - FocusManager.Instance.Focus(null); - e.Handled = true; - } - - /// - /// Called when the pointer is pressed anywhere on the window. - /// - /// The sender. - /// The event args. - private void TopLevelPreviewPointerPress(object sender, PointerPressedEventArgs e) - { - if (IsOpen) - { - var control = e.Source as ILogical; - - if (!this.IsLogicalParentOf(control)) - { - Close(); - } - } - } } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index cec653e045..dde689e89c 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Linq; using System.Windows.Input; using Avalonia.Controls.Generators; @@ -11,14 +12,13 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Threading; namespace Avalonia.Controls { /// /// A menu item control. /// - public class MenuItem : HeaderedSelectingItemsControl, ISelectable + public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable { /// /// Defines the property. @@ -62,6 +62,18 @@ namespace Avalonia.Controls public static readonly RoutedEvent ClickEvent = RoutedEvent.Register(nameof(Click), RoutingStrategies.Bubble); + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerEnterItemEvent = + RoutedEvent.Register(nameof(PointerEnterItem), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PointerLeaveItemEvent = + RoutedEvent.Register(nameof(PointerLeaveItem), RoutingStrategies.Bubble); + /// /// Defines the event. /// @@ -74,11 +86,6 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); - /// - /// The timer used to display submenus. - /// - private IDisposable _submenuTimer; - /// /// The submenu popup. /// @@ -93,16 +100,15 @@ namespace Avalonia.Controls CommandProperty.Changed.Subscribe(CommandChanged); FocusableProperty.OverrideDefaultValue(true); IconProperty.Changed.AddClassHandler(x => x.IconChanged); + IsSelectedProperty.Changed.AddClassHandler(x => x.IsSelectedChanged); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); ClickEvent.AddClassHandler(x => x.OnClick); SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); IsSubMenuOpenProperty.Changed.AddClassHandler(x => x.SubMenuOpenChanged); - AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler(x => x.AccessKeyPressed); } public MenuItem() { - } /// @@ -114,6 +120,30 @@ namespace Avalonia.Controls remove { RemoveHandler(ClickEvent, value); } } + /// + /// Occurs when the pointer enters a menu item. + /// + /// + /// A bubbling version of the event for menu items. + /// + public event EventHandler PointerEnterItem + { + add { AddHandler(PointerEnterItemEvent, value); } + remove { RemoveHandler(PointerEnterItemEvent, value); } + } + + /// + /// Raised when the pointer leaves a menu item. + /// + /// + /// A bubbling version of the event for menu items. + /// + public event EventHandler PointerLeaveItem + { + add { AddHandler(PointerLeaveItemEvent, value); } + remove { RemoveHandler(PointerLeaveItemEvent, value); } + } + /// /// Occurs when a 's submenu is opened. /// @@ -185,10 +215,71 @@ namespace Avalonia.Controls public bool HasSubMenu => !Classes.Contains(":empty"); /// - /// Gets a value that indicates whether the is a top-level menu item. + /// Gets a value that indicates whether the is a top-level main menu item. /// public bool IsTopLevel => Parent is Menu; + /// + bool IMenuItem.IsPointerOverSubMenu => _popup.PopupRoot?.IsPointerOver ?? false; + + /// + IMenuElement IMenuItem.Parent => Parent as IMenuElement; + + /// + bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); + + /// + IMenuItem IMenuElement.SelectedItem + { + get + { + var index = SelectedIndex; + return (index != -1) ? + (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : + null; + } + set + { + SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); + } + } + + /// + IEnumerable IMenuElement.SubItems + { + get + { + return ItemContainerGenerator.Containers + .Select(x => x.ContainerControl) + .OfType(); + } + } + + /// + /// Opens the submenu. + /// + /// + /// This has the same effect as setting to true. + /// + public void Open() => IsSubMenuOpen = true; + + /// + /// Closes the submenu. + /// + /// + /// This has the same effect as setting to false. + /// + public void Close() => IsSubMenuOpen = false; + + /// + void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent)); + + /// + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new MenuItemContainerGenerator(this); + } + /// /// Called when the is clicked. /// @@ -202,163 +293,43 @@ namespace Avalonia.Controls } } - /// - /// Called when the recieves focus. - /// - /// The event args. + /// protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); - IsSelected = true; + e.Handled = UpdateSelectionFromEventSource(e.Source, true); } /// - protected override IItemContainerGenerator CreateItemContainerGenerator() - { - return new MenuItemContainerGenerator(this); - } - - /// - /// Called when a key is pressed in the . - /// - /// The event args. protected override void OnKeyDown(KeyEventArgs e) { - // Some keypresses we want to pass straight to the parent MenuItem/Menu without giving - // this MenuItem the chance to handle them. This is usually e.g. when the submenu is - // closed so passing them to the base would try to move the selection in a hidden - // submenu. - var passStraightToParent = true; - - switch (e.Key) - { - case Key.Left: - if (!IsTopLevel && IsSubMenuOpen) - { - IsSubMenuOpen = false; - e.Handled = true; - } - - passStraightToParent = IsTopLevel || !IsSubMenuOpen; - break; - - case Key.Right: - if (!IsTopLevel && HasSubMenu && !IsSubMenuOpen) - { - SelectedIndex = 0; - IsSubMenuOpen = true; - e.Handled = true; - } - - passStraightToParent = IsTopLevel || !IsSubMenuOpen; - break; - - case Key.Enter: - if (HasSubMenu) - { - goto case Key.Right; - } - else - { - RaiseEvent(new RoutedEventArgs(ClickEvent)); - e.Handled = true; - } - - break; - - case Key.Escape: - if (IsSubMenuOpen) - { - IsSubMenuOpen = false; - e.Handled = true; - } - - break; - } - - if (!passStraightToParent) - { - base.OnKeyDown(e); - } + // Don't handle here: let event bubble up to menu. } - /// - /// Called when the pointer enters the . - /// - /// The event args. + /// protected override void OnPointerEnter(PointerEventArgs e) { base.OnPointerEnter(e); - var menu = Parent as Menu; - - if (menu != null) - { - if (menu.IsOpen) - { - IsSubMenuOpen = true; - } - } - else if (HasSubMenu && !IsSubMenuOpen) - { - _submenuTimer = DispatcherTimer.Run( - () => IsSubMenuOpen = true, - TimeSpan.FromMilliseconds(400)); - } - else + RaiseEvent(new PointerEventArgs { - var parentItem = Parent as MenuItem; - if (parentItem != null) - { - foreach (var sibling in parentItem.Items - .OfType() - .Where(x => x != this && x.IsSubMenuOpen)) - { - sibling.CloseSubmenus(); - sibling.IsSubMenuOpen = false; - sibling.IsSelected = false; - } - } - } + Device = e.Device, + RoutedEvent = PointerEnterItemEvent, + Source = this, + }); } - /// - /// Called when the pointer leaves the . - /// - /// The event args. + /// protected override void OnPointerLeave(PointerEventArgs e) { base.OnPointerLeave(e); - if (_submenuTimer != null) + RaiseEvent(new PointerEventArgs { - _submenuTimer.Dispose(); - _submenuTimer = null; - } - } - - /// - /// Called when the pointer is pressed over the . - /// - /// The event args. - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - base.OnPointerPressed(e); - - if (!HasSubMenu) - { - RaiseEvent(new RoutedEventArgs(ClickEvent)); - } - else if (IsTopLevel) - { - IsSubMenuOpen = !IsSubMenuOpen; - } - else - { - IsSubMenuOpen = true; - } - - e.Handled = true; + Device = e.Device, + RoutedEvent = PointerLeaveItemEvent, + Source = this, + }); } /// @@ -392,25 +363,6 @@ namespace Avalonia.Controls _popup.Closed += PopupClosed; } - /// - /// Called when the menu item's access key is pressed. - /// - /// The event args. - private void AccessKeyPressed(RoutedEventArgs e) - { - if (HasSubMenu) - { - SelectedIndex = 0; - IsSubMenuOpen = true; - } - else - { - RaiseEvent(new RoutedEventArgs(ClickEvent)); - } - - e.Handled = true; - } - /// /// Closes all submenus of the menu item. /// @@ -476,6 +428,18 @@ namespace Avalonia.Controls } } + /// + /// Called when the property changes. + /// + /// The property change event. + private void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e) + { + if ((bool)e.NewValue) + { + Focus(); + } + } + /// /// Called when the property changes. /// diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs new file mode 100644 index 0000000000..f65d3a4c72 --- /dev/null +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -0,0 +1,457 @@ +using System; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Avalonia.Rendering; +using Avalonia.Threading; + +namespace Avalonia.Controls.Platform +{ + /// + /// Provides the default keyboard and pointer interaction for menus. + /// + public class DefaultMenuInteractionHandler : IMenuInteractionHandler + { + private IDisposable _inputManagerSubscription; + private IRenderRoot _root; + + public DefaultMenuInteractionHandler() + : this(Input.InputManager.Instance, DefaultDelayRun) + { + } + + public DefaultMenuInteractionHandler( + IInputManager inputManager, + Action delayRun) + { + InputManager = inputManager; + DelayRun = delayRun; + } + + public virtual void Attach(IMenu menu) + { + if (Menu != null) + { + throw new NotSupportedException("DefaultMenuInteractionHandler is already attached."); + } + + Menu = menu; + Menu.GotFocus += GotFocus; + Menu.LostFocus += LostFocus; + Menu.KeyDown += KeyDown; + Menu.PointerPressed += PointerPressed; + Menu.PointerReleased += PointerReleased; + Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed); + Menu.AddHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); + Menu.AddHandler(MenuItem.PointerEnterItemEvent, PointerEnter); + Menu.AddHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); + + _root = Menu.VisualRoot; + + if (_root is InputElement inputRoot) + { + inputRoot.AddHandler(InputElement.PointerPressedEvent, RootPointerPressed, RoutingStrategies.Tunnel); + } + + if (_root is WindowBase window) + { + window.Deactivated += WindowDeactivated; + } + + _inputManagerSubscription = InputManager.Process.Subscribe(RawInput); + } + + public virtual void Detach(IMenu menu) + { + if (Menu != menu) + { + throw new NotSupportedException("DefaultMenuInteractionHandler is not attached to the menu."); + } + + Menu.GotFocus -= GotFocus; + Menu.LostFocus -= LostFocus; + Menu.KeyDown -= KeyDown; + Menu.PointerPressed -= PointerPressed; + Menu.PointerReleased -= PointerReleased; + Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed); + Menu.RemoveHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); + Menu.RemoveHandler(MenuItem.PointerEnterItemEvent, PointerEnter); + Menu.RemoveHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); + + if (_root is InputElement inputRoot) + { + inputRoot.RemoveHandler(InputElement.PointerPressedEvent, RootPointerPressed); + } + + if (_root is WindowBase root) + { + root.Deactivated -= WindowDeactivated; + } + + _inputManagerSubscription.Dispose(); + + Menu = null; + _root = null; + } + + protected Action DelayRun { get; } + + protected IInputManager InputManager { get; } + + protected IMenu Menu { get; private set; } + + protected static TimeSpan MenuShowDelay { get; } = TimeSpan.FromMilliseconds(400); + + protected internal virtual void GotFocus(object sender, GotFocusEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (item?.Parent != null) + { + item.SelectedItem = item; + } + } + + protected internal virtual void LostFocus(object sender, RoutedEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (item != null) + { + item.SelectedItem = null; + } + } + + protected internal virtual void KeyDown(object sender, KeyEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (item != null) + { + KeyDown(item, e); + } + } + + protected internal virtual void KeyDown(IMenuItem item, KeyEventArgs e) + { + switch (e.Key) + { + case Key.Up: + case Key.Down: + if (item.IsTopLevel) + { + if (item.HasSubMenu && !item.IsSubMenuOpen) + { + Open(item, true); + e.Handled = true; + } + } + else + { + goto default; + } + break; + + case Key.Left: + if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) + { + parent.Close(); + parent.Focus(); + e.Handled = true; + } + else + { + goto default; + } + break; + + case Key.Right: + if (!item.IsTopLevel && item.HasSubMenu) + { + Open(item, true); + e.Handled = true; + } + else + { + goto default; + } + break; + + case Key.Enter: + if (!item.HasSubMenu) + { + Click(item); + } + else + { + Open(item, true); + } + + e.Handled = true; + break; + + case Key.Escape: + if (item.Parent != null) + { + item.Parent.Close(); + item.Parent.Focus(); + e.Handled = true; + } + break; + + default: + var direction = e.Key.ToNavigationDirection(); + + if (direction.HasValue) + { + if (item.Parent?.MoveSelection(direction.Value, true) == true) + { + // If the the parent is an IMenu which successfully moved its selection, + // and the current menu is open then close the current menu and open the + // new menu. + if (item.IsSubMenuOpen && item.Parent is IMenu) + { + item.Close(); + Open(item.Parent.SelectedItem, true); + } + e.Handled = true; + } + } + + break; + } + + if (!e.Handled && item.Parent is IMenuItem parentItem) + { + KeyDown(parentItem, e); + } + } + + protected internal virtual void AccessKeyPressed(object sender, RoutedEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (item == null) + { + return; + } + + if (item.HasSubMenu) + { + Open(item, true); + } + else + { + Click(item); + } + + e.Handled = true; + } + + protected internal virtual void PointerEnter(object sender, PointerEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (item?.Parent == null) + { + return; + } + + if (item.IsTopLevel) + { + if (item.Parent.SelectedItem?.IsSubMenuOpen == true) + { + item.Parent.SelectedItem.Close(); + SelectItemAndAncestors(item); + Open(item, false); + } + else + { + SelectItemAndAncestors(item); + } + } + else + { + SelectItemAndAncestors(item); + + if (item.HasSubMenu) + { + OpenWithDelay(item); + } + else if (item.Parent != null) + { + foreach (var sibling in item.Parent.SubItems) + { + if (sibling.IsSubMenuOpen) + { + CloseWithDelay(sibling); + } + } + } + } + } + + protected internal virtual void PointerLeave(object sender, PointerEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (item?.Parent == null) + { + return; + } + + if (item.IsTopLevel) + { + if (!((IMenu)item.Parent).IsOpen && item.Parent.SelectedItem == item) + { + item.Parent.SelectedItem = null; + } + } + else if (!item.HasSubMenu) + { + item.Parent.SelectedItem = null; + } + } + + protected internal virtual void PointerPressed(object sender, PointerPressedEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (e.MouseButton == MouseButton.Left && item?.HasSubMenu == true) + { + Open(item, false); + e.Handled = true; + } + } + + protected internal virtual void PointerReleased(object sender, PointerReleasedEventArgs e) + { + var item = GetMenuItem(e.Source as IControl); + + if (e.MouseButton == MouseButton.Left && item.HasSubMenu == false) + { + Click(item); + e.Handled = true; + } + } + + protected internal virtual void MenuOpened(object sender, RoutedEventArgs e) + { + if (e.Source == Menu) + { + Menu.MoveSelection(NavigationDirection.First, true); + } + } + + protected internal virtual void RawInput(RawInputEventArgs e) + { + var mouse = e as RawMouseEventArgs; + + if (mouse?.Type == RawMouseEventType.NonClientLeftButtonDown) + { + Menu.Close(); + } + } + + protected internal virtual void RootPointerPressed(object sender, PointerPressedEventArgs e) + { + if (Menu?.IsOpen == true) + { + var control = e.Source as ILogical; + + if (!Menu.IsLogicalParentOf(control)) + { + Menu.Close(); + } + } + } + + protected internal virtual void WindowDeactivated(object sender, EventArgs e) + { + Menu.Close(); + } + + protected void Click(IMenuItem item) + { + item.RaiseClick(); + CloseMenu(item); + } + + protected void CloseMenu(IMenuItem item) + { + var current = (IMenuElement)item; + + while (current != null && !(current is IMenu)) + { + current = (current as IMenuItem).Parent; + } + + current?.Close(); + } + + protected void CloseWithDelay(IMenuItem item) + { + void Execute() + { + if (item.Parent?.SelectedItem != item) + { + item.Close(); + } + } + + DelayRun(Execute, MenuShowDelay); + } + + protected void Open(IMenuItem item, bool selectFirst) + { + item.Open(); + + if (selectFirst) + { + item.MoveSelection(NavigationDirection.First, true); + } + } + + protected void OpenWithDelay(IMenuItem item) + { + void Execute() + { + if (item.Parent?.SelectedItem == item) + { + Open(item, false); + } + } + + DelayRun(Execute, MenuShowDelay); + } + + protected void SelectItemAndAncestors(IMenuItem item) + { + var current = item; + + while (current?.Parent != null) + { + current.Parent.SelectedItem = current; + current = current.Parent as IMenuItem; + } + } + + protected static IMenuItem GetMenuItem(IControl item) + { + while (true) + { + if (item == null) + return null; + if (item is IMenuItem menuItem) + return menuItem; + item = item.Parent; + } + } + + private static void DefaultDelayRun(Action action, TimeSpan timeSpan) + { + DispatcherTimer.RunOnce(action, timeSpan); + } + } +} diff --git a/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs new file mode 100644 index 0000000000..342d3dd1c9 --- /dev/null +++ b/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs @@ -0,0 +1,22 @@ +using System; +using Avalonia.Input; + +namespace Avalonia.Controls.Platform +{ + /// + /// Handles user interaction for menus. + /// + public interface IMenuInteractionHandler + { + /// + /// Attaches the interaction handler to a menu. + /// + /// The menu. + void Attach(IMenu menu); + + /// + /// Detaches the interaction handler from the attached menu. + /// + void Detach(IMenu menu); + } +} diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index c8425a0f80..5451cf0701 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -457,6 +457,42 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Moves the selection in the specified direction relative to the current selection. + /// + /// The direction to move. + /// Whether to wrap when the selection reaches the first or last item. + /// True if the selection was moved; otherwise false. + protected bool MoveSelection(NavigationDirection direction, bool wrap) + { + var from = SelectedIndex != -1 ? ItemContainerGenerator.ContainerFromIndex(SelectedIndex) : null; + return MoveSelection(from, direction, wrap); + } + + /// + /// Moves the selection in the specified direction relative to the specified container. + /// + /// The container which serves as a starting point for the movement. + /// The direction to move. + /// Whether to wrap when the selection reaches the first or last item. + /// True if the selection was moved; otherwise false. + protected bool MoveSelection(IControl from, NavigationDirection direction, bool wrap) + { + if (Presenter?.Panel is INavigableContainer container && + GetNextControl(container, direction, from, wrap) is IControl next) + { + var index = ItemContainerGenerator.IndexFromContainer(next); + + if (index != -1) + { + SelectedIndex = index; + return true; + } + } + + return false; + } + /// /// Updates the selection for an item based on user interaction. /// diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index b0ccd8a3d1..645cdbd926 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -56,11 +56,49 @@ namespace Avalonia.Controls /// /// The movement direction. /// The control from which movement begins. + /// Whether to wrap around when the first or last item is reached. /// The control. - IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from, bool wrap) { - var fromControl = from as IControl; - return (fromControl != null) ? GetControlInDirection(direction, fromControl) : null; + var result = GetControlInDirection(direction, from as IControl); + + if (result == null && wrap) + { + if (Orientation == Orientation.Vertical) + { + switch (direction) + { + case NavigationDirection.Up: + case NavigationDirection.Previous: + case NavigationDirection.PageUp: + result = GetControlInDirection(NavigationDirection.Last, null); + break; + case NavigationDirection.Down: + case NavigationDirection.Next: + case NavigationDirection.PageDown: + result = GetControlInDirection(NavigationDirection.First, null); + break; + } + } + else + { + switch (direction) + { + case NavigationDirection.Left: + case NavigationDirection.Previous: + case NavigationDirection.PageUp: + result = GetControlInDirection(NavigationDirection.Last, null); + break; + case NavigationDirection.Right: + case NavigationDirection.Next: + case NavigationDirection.PageDown: + result = GetControlInDirection(NavigationDirection.First, null); + break; + } + } + } + + return result; } /// @@ -72,7 +110,7 @@ namespace Avalonia.Controls protected virtual IInputElement GetControlInDirection(NavigationDirection direction, IControl from) { var horiz = Orientation == Orientation.Horizontal; - int index = Children.IndexOf((IControl)from); + int index = from != null ? Children.IndexOf(from) : -1; switch (direction) { @@ -83,22 +121,22 @@ namespace Avalonia.Controls index = Children.Count - 1; break; case NavigationDirection.Next: - ++index; + if (index != -1) ++index; break; case NavigationDirection.Previous: - --index; + if (index != -1) --index; break; case NavigationDirection.Left: - index = horiz ? index - 1 : -1; + if (index != -1) index = horiz ? index - 1 : -1; break; case NavigationDirection.Right: - index = horiz ? index + 1 : -1; + if (index != -1) index = horiz ? index + 1 : -1; break; case NavigationDirection.Up: - index = horiz ? -1 : index - 1; + if (index != -1) index = horiz ? -1 : index - 1; break; case NavigationDirection.Down: - index = horiz ? -1 : index + 1; + if (index != -1) index = horiz ? -1 : index + 1; break; default: index = -1; diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 745de95bca..84d3cc791e 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -47,8 +47,9 @@ namespace Avalonia.Controls /// /// The movement direction. /// The control from which movement begins. + /// Whether to wrap around when the first or last item is reached. /// The control. - IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from, bool wrap) { var horiz = Orientation == Orientation.Horizontal; int index = Children.IndexOf((IControl)from); @@ -250,4 +251,4 @@ namespace Avalonia.Controls } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 7baa4103d7..b78e5a9f98 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -53,10 +53,32 @@ namespace Avalonia.Input /// private IInputElement _restoreFocusElement; + /// + /// The window's main menu. + /// + private IMainMenu _mainMenu; + /// /// Gets or sets the window's main menu. /// - public IMainMenu MainMenu { get; set; } + public IMainMenu MainMenu + { + get => _mainMenu; + set + { + if (_mainMenu != null) + { + _mainMenu.MenuClosed -= MainMenuClosed; + } + + _mainMenu = value; + + if (_mainMenu != null) + { + _mainMenu.MenuClosed += MainMenuClosed; + } + } + } /// /// Sets the owner of the access key handler. @@ -160,13 +182,7 @@ namespace Avalonia.Input { bool menuIsOpen = MainMenu?.IsOpen == true; - if (e.Key == Key.Escape && menuIsOpen) - { - // When the Escape key is pressed with the main menu open, close it. - CloseMenu(); - e.Handled = true; - } - else if ((e.Modifiers & InputModifiers.Alt) != 0 || menuIsOpen) + if ((e.Modifiers & InputModifiers.Alt) != 0 || menuIsOpen) { // If any other key is pressed with the Alt key held down, or the main menu is open, // find all controls who have registered that access key. @@ -245,5 +261,10 @@ namespace Avalonia.Input MainMenu.Close(); _owner.ShowAccessKeys = _showingAccessKeys = false; } + + private void MainMenuClosed(object sender, EventArgs e) + { + _owner.ShowAccessKeys = false; + } } } diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs index 5acb6aa777..c9924dbffb 100644 --- a/src/Avalonia.Input/IInputElement.cs +++ b/src/Avalonia.Input/IInputElement.cs @@ -15,7 +15,7 @@ namespace Avalonia.Input /// /// Occurs when the control receives focus. /// - event EventHandler GotFocus; + event EventHandler GotFocus; /// /// Occurs when the control loses focus. diff --git a/src/Avalonia.Input/IMainMenu.cs b/src/Avalonia.Input/IMainMenu.cs index 39d19a0a76..a3373191a8 100644 --- a/src/Avalonia.Input/IMainMenu.cs +++ b/src/Avalonia.Input/IMainMenu.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using Avalonia.Interactivity; using Avalonia.VisualTree; namespace Avalonia.Input @@ -24,5 +26,10 @@ namespace Avalonia.Input /// Opens the menu in response to the Alt/F10 key. /// void Open(); + + /// + /// Occurs when the main menu closes. + /// + event EventHandler MenuClosed; } } diff --git a/src/Avalonia.Input/INavigableContainer.cs b/src/Avalonia.Input/INavigableContainer.cs index 13d734bd0b..df434bca70 100644 --- a/src/Avalonia.Input/INavigableContainer.cs +++ b/src/Avalonia.Input/INavigableContainer.cs @@ -13,7 +13,8 @@ namespace Avalonia.Input /// /// The movement direction. /// The control from which movement begins. + /// Whether to wrap around when the first or last item is reached. /// The control. - IInputElement GetControl(NavigationDirection direction, IInputElement from); + IInputElement GetControl(NavigationDirection direction, IInputElement from, bool wrap); } } diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 82e626f9c6..3aff5d0a8b 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -177,7 +177,7 @@ namespace Avalonia.Input /// /// Occurs when the control receives focus. /// - public event EventHandler GotFocus + public event EventHandler GotFocus { add { AddHandler(GotFocusEvent, value); } remove { RemoveHandler(GotFocusEvent, value); } diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index a9d5b83073..18db2a9173 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -168,7 +168,7 @@ namespace Avalonia.Input.Navigation { while (element != null) { - element = navigable.GetControl(direction, element); + element = navigable.GetControl(direction, element, false); if (element != null && element.CanFocus()) { diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 5404117363..8a2ed2a802 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -127,11 +127,6 @@ - - diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs new file mode 100644 index 0000000000..fd4aea47a3 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -0,0 +1,489 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Platform +{ + public class DefaultMenuInteractionHandlerTests + { + public class TopLevel + { + [Fact] + public void Up_Opens_MenuItem_With_SubMenu() + { + var target = new DefaultMenuInteractionHandler(); + var item = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var e = new KeyEventArgs { Key = Key.Up, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Down_Opens_MenuItem_With_SubMenu() + { + var target = new DefaultMenuInteractionHandler(); + var item = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var e = new KeyEventArgs { Key = Key.Down, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Right_Selects_Next_MenuItem() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(x => x.MoveSelection(NavigationDirection.Right, true) == true); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); + var e = new KeyEventArgs { Key = Key.Right, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(menu).Verify(x => x.MoveSelection(NavigationDirection.Right, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Left_Selects_Previous_MenuItem() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(x => x.MoveSelection(NavigationDirection.Left, true) == true); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); + var e = new KeyEventArgs { Key = Key.Left, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(menu).Verify(x => x.MoveSelection(NavigationDirection.Left, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Enter_On_Item_With_No_SubMenu_Causes_Click() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); + var e = new KeyEventArgs { Key = Key.Enter, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.RaiseClick()); + Mock.Get(menu).Verify(x => x.Close()); + Assert.True(e.Handled); + } + + [Fact] + public void Enter_On_Item_With_SubMenu_Opens_SubMenu() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var item = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var e = new KeyEventArgs { Key = Key.Enter, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Escape_Closes_Parent_Menu() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); + var e = new KeyEventArgs { Key = Key.Escape, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(menu).Verify(x => x.Close()); + Assert.True(e.Handled); + } + + [Fact] + public void PointerEnter_Opens_Item_When_Old_Item_Is_Open() + { + var target = new DefaultMenuInteractionHandler(); + var menu = new Mock(); + var item = Mock.Of(x => + x.IsSubMenuOpen == true && + x.IsTopLevel == true && + x.HasSubMenu == true && + x.Parent == menu.Object); + var nextItem = Mock.Of(x => + x.IsTopLevel == true && + x.HasSubMenu == true && + x.Parent == menu.Object); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = nextItem }; + + menu.SetupGet(x => x.SelectedItem).Returns(item); + + target.PointerEnter(nextItem, e); + + Mock.Get(item).Verify(x => x.Close()); + menu.VerifySet(x => x.SelectedItem = nextItem); + Mock.Get(nextItem).Verify(x => x.Open()); + Mock.Get(nextItem).Verify(x => x.MoveSelection(NavigationDirection.First, true), Times.Never); + Assert.False(e.Handled); + + } + + [Fact] + public void PointerLeave_Deselects_Item_When_Menu_Not_Open() + { + var target = new DefaultMenuInteractionHandler(); + var menu = new Mock(); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + + menu.SetupGet(x => x.SelectedItem).Returns(item); + target.PointerLeave(item, e); + + menu.VerifySet(x => x.SelectedItem = null); + Assert.False(e.Handled); + } + + [Fact] + public void PointerLeave_Doesnt_Deselect_Item_When_Menu_Open() + { + var target = new DefaultMenuInteractionHandler(); + var menu = new Mock(); + var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + + menu.SetupGet(x => x.IsOpen).Returns(true); + menu.SetupGet(x => x.SelectedItem).Returns(item); + target.PointerLeave(item, e); + + menu.VerifySet(x => x.SelectedItem = null, Times.Never); + Assert.False(e.Handled); + } + } + + public class NonTopLevel + { + [Fact] + public void Up_Selects_Previous_MenuItem() + { + var target = new DefaultMenuInteractionHandler(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new KeyEventArgs { Key = Key.Up, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(parentItem).Verify(x => x.MoveSelection(NavigationDirection.Up, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Down_Selects_Next_MenuItem() + { + var target = new DefaultMenuInteractionHandler(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new KeyEventArgs { Key = Key.Down, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(parentItem).Verify(x => x.MoveSelection(NavigationDirection.Down, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Left_Closes_Parent_SubMenu() + { + var target = new DefaultMenuInteractionHandler(); + var parentItem = Mock.Of(x => x.HasSubMenu == true && x.IsSubMenuOpen == true); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new KeyEventArgs { Key = Key.Left, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(parentItem).Verify(x => x.Close()); + Mock.Get(parentItem).Verify(x => x.Focus()); + Assert.True(e.Handled); + } + + [Fact] + public void Right_With_SubMenu_Items_Opens_SubMenu() + { + var target = new DefaultMenuInteractionHandler(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); + var e = new KeyEventArgs { Key = Key.Right, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Right_On_TopLevel_Child_Navigates_TopLevel_Selection() + { + var target = new DefaultMenuInteractionHandler(); + var menu = new Mock(); + var parentItem = Mock.Of(x => + x.IsSubMenuOpen == true && + x.IsTopLevel == true && + x.HasSubMenu == true && + x.Parent == menu.Object); + var nextItem = Mock.Of(x => + x.IsTopLevel == true && + x.HasSubMenu == true && + x.Parent == menu.Object); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new KeyEventArgs { Key = Key.Right, Source = item }; + + menu.Setup(x => x.MoveSelection(NavigationDirection.Right, true)) + .Callback(() => menu.SetupGet(x => x.SelectedItem).Returns(nextItem)) + .Returns(true); + + target.KeyDown(item, e); + + menu.Verify(x => x.MoveSelection(NavigationDirection.Right, true)); + Mock.Get(parentItem).Verify(x => x.Close()); + Mock.Get(nextItem).Verify(x => x.Open()); + Mock.Get(nextItem).Verify(x => x.MoveSelection(NavigationDirection.First, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Enter_On_Item_With_No_SubMenu_Causes_Click() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new KeyEventArgs { Key = Key.Enter, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.RaiseClick()); + Mock.Get(menu).Verify(x => x.Close()); + Assert.True(e.Handled); + } + + [Fact] + public void Enter_On_Item_With_SubMenu_Opens_SubMenu() + { + var target = new DefaultMenuInteractionHandler(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); + var e = new KeyEventArgs { Key = Key.Enter, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); + Assert.True(e.Handled); + } + + [Fact] + public void Escape_Closes_Parent_MenuItem() + { + var target = new DefaultMenuInteractionHandler(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new KeyEventArgs { Key = Key.Escape, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(parentItem).Verify(x => x.Close()); + Mock.Get(parentItem).Verify(x => x.Focus()); + Assert.True(e.Handled); + } + + [Fact] + public void PointerEnter_Selects_Item() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + + target.PointerEnter(item, e); + + Mock.Get(parentItem).VerifySet(x => x.SelectedItem = item); + Assert.False(e.Handled); + } + + [Fact] + public void PointerEnter_Opens_Submenu_After_Delay() + { + var timer = new TestTimer(); + var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + + target.PointerEnter(item, e); + Mock.Get(item).Verify(x => x.Open(), Times.Never); + + timer.Pulse(); + Mock.Get(item).Verify(x => x.Open()); + + Assert.False(e.Handled); + } + + [Fact] + public void PointerEnter_Closes_Sibling_Submenu_After_Delay() + { + var timer = new TestTimer(); + var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem); + var sibling = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsSubMenuOpen == true); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + + Mock.Get(parentItem).SetupGet(x => x.SubItems).Returns(new[] { item, sibling }); + + target.PointerEnter(item, e); + Mock.Get(sibling).Verify(x => x.Close(), Times.Never); + + timer.Pulse(); + Mock.Get(sibling).Verify(x => x.Close()); + + Assert.False(e.Handled); + } + + [Fact] + public void PointerLeave_Deselects_Item() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + + target.PointerLeave(item, e); + + Mock.Get(parentItem).VerifySet(x => x.SelectedItem = null); + Assert.False(e.Handled); + } + + [Fact] + public void PointerLeave_Doesnt_Deselect_Item_If_Pointer_Over_Submenu() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsPointerOverSubMenu == true); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + + target.PointerLeave(item, e); + + Mock.Get(parentItem).VerifySet(x => x.SelectedItem = null, Times.Never); + Assert.False(e.Handled); + } + + [Fact] + public void PointerReleased_On_Item_With_No_SubMenu_Causes_Click() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem); + var e = new PointerReleasedEventArgs { MouseButton = MouseButton.Left, Source = item }; + + target.PointerReleased(item, e); + + Mock.Get(item).Verify(x => x.RaiseClick()); + Mock.Get(menu).Verify(x => x.Close()); + Assert.True(e.Handled); + } + + [Fact] + public void Selection_Is_Correct_When_Pointer_Temporarily_Exits_Item_To_Select_SubItem() + { + var timer = new TestTimer(); + var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); + var childItem = Mock.Of(x => x.Parent == item); + var enter = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + var leave = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + + // Pointer enters item; item is selected. + target.PointerEnter(item, enter); + Assert.True(timer.ActionIsQueued); + Mock.Get(parentItem).VerifySet(x => x.SelectedItem = item); + Mock.Get(parentItem).ResetCalls(); + + // SubMenu shown after a delay. + timer.Pulse(); + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).SetupGet(x => x.IsSubMenuOpen).Returns(true); + Mock.Get(item).ResetCalls(); + + // Pointer briefly exits item, but submenu remains open. + target.PointerLeave(item, leave); + Mock.Get(item).Verify(x => x.Close(), Times.Never); + Mock.Get(item).ResetCalls(); + + // Pointer enters child item; is selected. + enter.Source = childItem; + target.PointerEnter(childItem, enter); + Mock.Get(item).VerifySet(x => x.SelectedItem = childItem); + Mock.Get(parentItem).VerifySet(x => x.SelectedItem = item); + Mock.Get(item).ResetCalls(); + Mock.Get(parentItem).ResetCalls(); + } + + [Fact] + public void PointerPressed_On_Item_With_SubMenu_Causes_Opens_Submenu() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); + var e = new PointerPressedEventArgs { MouseButton = MouseButton.Left, Source = item }; + + target.PointerPressed(item, e); + + Mock.Get(item).Verify(x => x.Open()); + Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true), Times.Never); + Assert.True(e.Handled); + } + } + + private class TestTimer + { + private Action _action; + + public bool ActionIsQueued => _action != null; + + public void Pulse() + { + _action(); + _action = null; + } + + public void RunOnce(Action action, TimeSpan timeSpan) + { + if (_action != null) + { + throw new NotSupportedException("Action already set."); + } + + _action = action; + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index b0ae3df8a2..70a40faed3 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -219,7 +219,7 @@ namespace Avalonia.Controls.UnitTests scrollable.Setup(x => x.IsLogicalScrollEnabled).Returns(true); ((ISetLogicalParent)target).SetParent(presenter.Object); - ((INavigableContainer)target).GetControl(NavigationDirection.Next, from); + ((INavigableContainer)target).GetControl(NavigationDirection.Next, from, false); scrollable.Verify(x => x.GetControlInDirection(NavigationDirection.Next, from)); } From e5662a8a18cbfeb795c6dc9038e677b89cb9702e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 14 Aug 2018 13:16:28 +0200 Subject: [PATCH 07/23] Implement ContextMenu interactions. --- src/Avalonia.Controls/ContextMenu.cs | 148 +++++++++++++++++---------- 1 file changed, 96 insertions(+), 52 deletions(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 13f00bdc87..0accb284b6 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,16 +1,18 @@ +using System; +using System.Reactive.Linq; +using System.Linq; +using System.ComponentModel; +using Avalonia.Controls.Platform; +using System.Collections.Generic; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.Controls.Primitives; + namespace Avalonia.Controls { - using Input; - using Interactivity; - using LogicalTree; - using Primitives; - using System; - using System.Reactive.Linq; - using System.Linq; - using System.ComponentModel; - - public class ContextMenu : SelectingItemsControl + public class ContextMenu : SelectingItemsControl, IMenu { + private readonly IMenuInteractionHandler _interaction; private bool _isOpen; private Popup _popup; @@ -20,6 +22,25 @@ namespace Avalonia.Controls public static readonly DirectProperty IsOpenProperty = AvaloniaProperty.RegisterDirect(nameof(IsOpen), o => o.IsOpen); + /// + /// Initializes a new instance of the class. + /// + public ContextMenu() + { + _interaction = AvaloniaLocator.Current.GetService() ?? + new DefaultMenuInteractionHandler(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The menu iteraction handler. + public ContextMenu(IMenuInteractionHandler interactionHandler) + { + Contract.Requires(interactionHandler != null); + + _interaction = interactionHandler; + } /// /// Initializes static members of the class. @@ -27,8 +48,6 @@ namespace Avalonia.Controls static ContextMenu() { ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); - - MenuItem.ClickEvent.AddClassHandler(x => x.OnContextMenuClick, handledEventsToo: true); } /// @@ -36,6 +55,36 @@ namespace Avalonia.Controls /// public bool IsOpen => _isOpen; + /// + IMenuInteractionHandler IMenu.InteractionHandler => _interaction; + + /// + IMenuItem IMenuElement.SelectedItem + { + get + { + var index = SelectedIndex; + return (index != -1) ? + (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) : + null; + } + set + { + SelectedIndex = ItemContainerGenerator.IndexFromContainer(value); + } + } + + /// + IEnumerable IMenuElement.SubItems + { + get + { + return ItemContainerGenerator.Containers + .Select(x => x.ContainerControl) + .OfType(); + } + } + /// /// Occurs when the value of the /// @@ -50,7 +99,6 @@ namespace Avalonia.Controls /// public event CancelEventHandler ContextMenuClosing; - /// /// Called when the property changes on a control. /// @@ -71,62 +119,53 @@ namespace Avalonia.Controls } /// - /// Called when a submenu is clicked somewhere in the menu. + /// Opens the menu. /// - /// The event args. - private void OnContextMenuClick(RoutedEventArgs e) - { - Hide(); - FocusManager.Instance.Focus(null); - e.Handled = true; - } + public void Open() => Open(null); /// - /// Closes the menu. + /// Opens a context menu on the specified control. /// - public void Hide() + /// The control. + public void Open(Control control) { - if (_popup != null && _popup.IsVisible) + if (_popup == null) { - _popup.IsOpen = false; + _popup = new Popup() + { + PlacementMode = PlacementMode.Pointer, + PlacementTarget = control, + StaysOpen = false, + ObeyScreenEdges = true + }; + + _popup.Closed += PopupClosed; + _interaction.Attach(this); } - SelectedIndex = -1; + ((ISetLogicalParent)_popup).SetParent(control); + _popup.Child = this; + _popup.IsOpen = true; - SetAndRaise(IsOpenProperty, ref _isOpen, false); + SetAndRaise(IsOpenProperty, ref _isOpen, true); } /// - /// Shows a context menu for the specified control. + /// Closes the menu. /// - /// The control. - private void Show(Control control) + public void Close() { - if (control != null) + if (_popup != null && _popup.IsVisible) { - if (_popup == null) - { - _popup = new Popup() - { - PlacementMode = PlacementMode.Pointer, - PlacementTarget = control, - StaysOpen = false, - ObeyScreenEdges = true - }; - - _popup.Closed += PopupClosed; - } - - ((ISetLogicalParent)_popup).SetParent(control); - _popup.Child = this; + _popup.IsOpen = false; + } - _popup.IsOpen = true; + SelectedIndex = -1; - SetAndRaise(IsOpenProperty, ref _isOpen, true); - } + SetAndRaise(IsOpenProperty, ref _isOpen, false); } - private static void PopupClosed(object sender, EventArgs e) + private void PopupClosed(object sender, EventArgs e) { var contextMenu = (sender as Popup)?.Child as ContextMenu; @@ -152,7 +191,7 @@ namespace Avalonia.Controls if (contextMenu.CancelClosing()) return; - control.ContextMenu.Hide(); + control.ContextMenu.Close(); e.Handled = true; } @@ -161,7 +200,7 @@ namespace Avalonia.Controls if (contextMenu.CancelOpening()) return; - contextMenu.Show(control); + contextMenu.Open(control); e.Handled = true; } } @@ -179,5 +218,10 @@ namespace Avalonia.Controls ContextMenuOpening?.Invoke(this, eventArgs); return eventArgs.Cancel; } + + bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) + { + throw new NotImplementedException(); + } } } From 0fb1780f75ff4fc9ec1540578206383e717855b9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 14 Aug 2018 13:40:03 +0200 Subject: [PATCH 08/23] Don't deselect sibling item on PointerLeave. --- .../Platform/DefaultMenuInteractionHandler.cs | 15 +++++++++------ .../DefaultMenuInteractionHandlerTests.cs | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index f65d3a4c72..3cd3094483 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -301,17 +301,20 @@ namespace Avalonia.Controls.Platform return; } - if (item.IsTopLevel) + if (item.Parent.SelectedItem == item) { - if (!((IMenu)item.Parent).IsOpen && item.Parent.SelectedItem == item) + if (item.IsTopLevel) + { + if (!((IMenu)item.Parent).IsOpen) + { + item.Parent.SelectedItem = null; + } + } + else if (!item.HasSubMenu) { item.Parent.SelectedItem = null; } } - else if (!item.HasSubMenu) - { - item.Parent.SelectedItem = null; - } } protected internal virtual void PointerPressed(object sender, PointerPressedEventArgs e) diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index fd4aea47a3..e17279013d 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -371,12 +371,30 @@ namespace Avalonia.Controls.UnitTests.Platform var item = Mock.Of(x => x.Parent == parentItem); var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(item); target.PointerLeave(item, e); Mock.Get(parentItem).VerifySet(x => x.SelectedItem = null); Assert.False(e.Handled); } + [Fact] + public void PointerLeave_Doesnt_Deselect_Sibling() + { + var target = new DefaultMenuInteractionHandler(); + var menu = Mock.Of(); + var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); + var item = Mock.Of(x => x.Parent == parentItem); + var sibling = Mock.Of(x => x.Parent == parentItem); + var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + + Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(sibling); + target.PointerLeave(item, e); + + Mock.Get(parentItem).VerifySet(x => x.SelectedItem = null, Times.Never); + Assert.False(e.Handled); + } + [Fact] public void PointerLeave_Doesnt_Deselect_Item_If_Pointer_Over_Submenu() { From c7bf1ecb4bbfa8f8ffc92d23958049fe4e23dc03 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 14 Aug 2018 21:14:54 +0200 Subject: [PATCH 09/23] Fix null checking. --- .../Platform/DefaultMenuInteractionHandler.cs | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 3cd3094483..a44495b90c 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -135,6 +135,8 @@ namespace Avalonia.Controls.Platform protected internal virtual void KeyDown(IMenuItem item, KeyEventArgs e) { + Contract.Requires(item != null); + switch (e.Key) { case Key.Up: @@ -154,7 +156,7 @@ namespace Avalonia.Controls.Platform break; case Key.Left: - if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) + if (item.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) { parent.Close(); parent.Focus(); @@ -203,20 +205,17 @@ namespace Avalonia.Controls.Platform default: var direction = e.Key.ToNavigationDirection(); - if (direction.HasValue) + if (direction.HasValue && item.Parent?.MoveSelection(direction.Value, true) == true) { - if (item.Parent?.MoveSelection(direction.Value, true) == true) + // If the the parent is an IMenu which successfully moved its selection, + // and the current menu is open then close the current menu and open the + // new menu. + if (item.IsSubMenuOpen && item.Parent is IMenu) { - // If the the parent is an IMenu which successfully moved its selection, - // and the current menu is open then close the current menu and open the - // new menu. - if (item.IsSubMenuOpen && item.Parent is IMenu) - { - item.Close(); - Open(item.Parent.SelectedItem, true); - } - e.Handled = true; + item.Close(); + Open(item.Parent.SelectedItem, true); } + e.Handled = true; } break; @@ -387,7 +386,7 @@ namespace Avalonia.Controls.Platform while (current != null && !(current is IMenu)) { - current = (current as IMenuItem).Parent; + current = (current as IMenuItem)?.Parent; } current?.Close(); From 47676e6be24ee654307ac216c5cf83feb5fda682 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 18 Aug 2018 14:46:26 +0100 Subject: [PATCH 10/23] fix null reference exception. --- src/Avalonia.Controls/MenuItem.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index dde689e89c..91f427936c 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -368,9 +368,12 @@ namespace Avalonia.Controls /// private void CloseSubmenus() { - foreach (var child in Items.OfType()) + if (Items != null) { - child.IsSubMenuOpen = false; + foreach (var child in Items.OfType()) + { + child.IsSubMenuOpen = false; + } } } From ca34bf9aba582e101d65a4ccf68e0e530c5cf73d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 26 Aug 2018 19:37:27 +0200 Subject: [PATCH 11/23] Don't assume Items are MenuItems. They may be created via `DataTemplate`s. --- src/Avalonia.Controls/MenuItem.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 91f427936c..7b57783c5a 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -342,7 +342,7 @@ namespace Avalonia.Controls if (menuItem != null && menuItem.Parent == this) { - foreach (var child in Items.OfType()) + foreach (var child in ((IMenuItem)this).SubItems) { if (child != menuItem && child.IsSubMenuOpen) { @@ -368,12 +368,9 @@ namespace Avalonia.Controls /// private void CloseSubmenus() { - if (Items != null) + foreach (var child in ((IMenuItem)this).SubItems) { - foreach (var child in Items.OfType()) - { - child.IsSubMenuOpen = false; - } + child.IsSubMenuOpen = false; } } From 4e84b1e487f08453e88a653e53104c967aa22cc1 Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Mon, 27 Aug 2018 19:04:17 +0200 Subject: [PATCH 12/23] Made sure that only the required assemblies are copied to the appropriate package directories + included net461 package output. --- scripts/ReplaceNugetCache.ps1 | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/ReplaceNugetCache.ps1 b/scripts/ReplaceNugetCache.ps1 index a03d442bff..6de50f978d 100644 --- a/scripts/ReplaceNugetCache.ps1 +++ b/scripts/ReplaceNugetCache.ps1 @@ -1,5 +1,6 @@ +copy ..\samples\ControlCatalog.Desktop\bin\Debug\net461\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\net461\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ -copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Gtk3.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ From d72ca725cde7639d66c58a899530f6ba45f423f5 Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Mon, 27 Aug 2018 19:27:19 +0200 Subject: [PATCH 13/23] Brought RelativeSource=Self behavior in line with other RelativeSources -Direct fix for AnimationKeyFrame using itself as self-reference --- src/Markup/Avalonia.Markup/Data/Binding.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 4f18c682b4..cb43873cee 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -150,7 +150,9 @@ namespace Avalonia.Data } else if (RelativeSource.Mode == RelativeSourceMode.Self) { - observer = CreateSourceObserver(target, node); + observer = CreateSourceObserver( + (target as IStyledElement) ?? (anchor as IStyledElement), + node); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { From 8e7d2e5a814f13d4f7cb5258a47a483511aadb9a Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Wed, 29 Aug 2018 21:39:51 +0200 Subject: [PATCH 14/23] Handle the case of multiple content presenters within a content control handled via ContentControlMixin --- src/Avalonia.Controls/Mixins/ContentControlMixin.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs index 95193c0432..e4204bd27f 100644 --- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs +++ b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Reactive.Disposables; using System.Runtime.CompilerServices; using Avalonia.Collections; using Avalonia.Controls.Presenters; @@ -75,6 +76,12 @@ namespace Avalonia.Controls.Mixins null, presenter.GetValue(ContentPresenter.ChildProperty)); + if (subscriptions.Value.TryGetValue(sender, out IDisposable previousSubscription)) + { + subscription = new CompositeDisposable(previousSubscription, subscription); + subscriptions.Value.Remove(sender); + } + subscriptions.Value.Add(sender, subscription); } } From a14afe5c2c61ec80fb4689d535a0466bdb75120c Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Thu, 30 Aug 2018 19:00:23 +0200 Subject: [PATCH 15/23] Unit tests for the mixin changes --- .../Mixins/ContentControlMixinTests.cs | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs new file mode 100644 index 0000000000..a0487842a9 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs @@ -0,0 +1,136 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; +using Avalonia.Collections; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.LogicalTree; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Mixins +{ + public class ContentControlMixinTests + { + [Fact] + public void Multiple_Mixin_Usages_Should_Not_Throw() + { + var target = new TestControl() + { + Template = new FuncControlTemplate(_ => new Panel + { + Children = + { + new ContentPresenter { Name = "Content_1_Presenter" }, + new ContentPresenter { Name = "Content_2_Presenter" } + } + }) + }; + + var ex = Record.Exception(() => target.ApplyTemplate()); + + Assert.Null(ex); + } + + [Fact] + public void Replacing_Template_Releases_Events() + { + var p1 = new ContentPresenter { Name = "Content_1_Presenter" }; + var p2 = new ContentPresenter { Name = "Content_2_Presenter" }; + + var callIndex = -1; + var called = new bool[4]; + + void Callback() + { + if (callIndex >= 0) + called[callIndex] = true; + } + + var listMock = new Mock>(); + listMock.Setup(l => l.Contains(It.IsAny())).Returns(false).Callback(Callback); + var list = listMock.Object; + + var target = new TestControl(list) + { + Template = new FuncControlTemplate(_ => new Panel + { + Children = + { + p1, + p2 + } + }) + }; + target.ApplyTemplate(); + + callIndex = 0; + p1.Content = new Control(); + p1.UpdateChild(); + + callIndex = 1; + p2.Content = new Control(); + p2.UpdateChild(); + + target.Template = null; + + callIndex = 2; + p1.Content = new Control(); + p1.UpdateChild(); + + callIndex = 3; + p2.Content = new Control(); + p2.UpdateChild(); + + + Assert.Equal(new[] { true, true, false, false }, called); + } + + private class TestControl : TemplatedControl + { + public static readonly StyledProperty Content1Property = + AvaloniaProperty.Register(nameof(Content1)); + + public static readonly StyledProperty Content2Property = + AvaloniaProperty.Register(nameof(Content2)); + + + static TestControl() + { + ContentControlMixin.Attach(Content1Property, x => x.GetLogicalChildren(), "Content_1_Presenter"); + ContentControlMixin.Attach(Content2Property, x => x.GetLogicalChildren(), "Content_2_Presenter"); + } + + private IAvaloniaList _mock; + + public TestControl() + { + } + + public TestControl(IAvaloniaList mock) + { + _mock = mock; + } + + public IAvaloniaList GetLogicalChildren() + { + return _mock ?? LogicalChildren; + } + + public object Content1 + { + get { return GetValue(Content1Property); } + set { SetValue(Content1Property, value); } + } + + public object Content2 + { + get { return GetValue(Content2Property); } + set { SetValue(Content2Property, value); } + } + } + } +} From 49f36d4ac60d3ce2adf0e9a0f9a08f95c2eeb2cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Marki=C4=8D?= Date: Sat, 1 Sep 2018 12:56:03 +0200 Subject: [PATCH 16/23] Adds reference to nightly build feed --- readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readme.md b/readme.md index 345ad7fe9b..f345cbd9df 100644 --- a/readme.md +++ b/readme.md @@ -35,6 +35,9 @@ Install-Package Avalonia.Desktop Try out the latest build of Avalonia available for download here: https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master/artifacts +or use nightly build feeds as described here: +https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed + ## Documentation As mentioned above, Avalonia is still in beta and as such there's not much documentation yet. You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia). From f797c1d6c10832811632117e6a0d70fcfdf6e2a2 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 1 Sep 2018 09:17:54 -0700 Subject: [PATCH 17/23] Fix Avalonia.Android output path in packages.cake. VS 15.8 changed the output directory for Android projects. We need this change to match the new behavior. --- packages.cake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages.cake b/packages.cake index d633230189..9defa3004c 100644 --- a/packages.cake +++ b/packages.cake @@ -303,7 +303,7 @@ public class Packages { new NuSpecContent { Source = "Avalonia.Android.dll", Target = "lib/MonoAndroid10" } }, - BasePath = context.Directory("./src/Android/Avalonia.Android/bin/" + parameters.DirSuffix + "/monoandroid44/"), + BasePath = context.Directory("./src/Android/Avalonia.Android/bin/" + parameters.DirSuffix + "/monoandroid44/MonoAndroid44/"), OutputDirectory = parameters.NugetRoot }, /////////////////////////////////////////////////////////////////////////////// From 1524cb1aba4ca188d04fe313841435e2536d4be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Marki=C4=8D?= Date: Sat, 1 Sep 2018 20:03:07 +0200 Subject: [PATCH 18/23] Spellchecks comments and members, removes unused using statements and sorts them --- src/Avalonia.Controls/AppBuilderBase.cs | 6 ++--- src/Avalonia.Controls/Application.cs | 1 - src/Avalonia.Controls/AutoCompleteBox.cs | 14 +++++------ src/Avalonia.Controls/Border.cs | 3 +-- src/Avalonia.Controls/Calendar/Calendar.cs | 6 ++--- .../CalendarBlackoutDatesCollection.cs | 3 +-- .../Calendar/CalendarDateRange.cs | 1 - .../Calendar/CalendarDayButton.cs | 4 +--- .../Calendar/CalendarExtensions.cs | 3 --- .../Calendar/CalendarItem.cs | 6 ++--- src/Avalonia.Controls/Calendar/DatePicker.cs | 11 ++++----- src/Avalonia.Controls/ContextMenu.cs | 2 +- src/Avalonia.Controls/Control.cs | 16 ++----------- src/Avalonia.Controls/DataValidationErrors.cs | 1 - .../Embedding/EmbeddableControlRoot.cs | 1 - .../Embedding/Offscreen/OffscreenTopLevel.cs | 5 ---- .../Offscreen/OffscreenTopLevelImpl.cs | 3 --- src/Avalonia.Controls/Expander.cs | 1 - .../Generators/IItemContainerGenerator.cs | 4 ++-- .../Generators/ITreeItemContainerGenerator.cs | 4 +--- .../Generators/ItemContainerGenerator`1.cs | 2 -- .../Generators/MenuItemContainerGenerator.cs | 8 +------ .../Generators/TreeContainerIndex.cs | 2 +- .../Generators/TreeItemContainerGenerator.cs | 1 - src/Avalonia.Controls/GridLength.cs | 5 ++-- src/Avalonia.Controls/GridSplitter.cs | 16 ++++++------- src/Avalonia.Controls/HotkeyManager.cs | 5 ---- .../IApplicationLifecycle.cs | 4 ---- src/Avalonia.Controls/IControl.cs | 5 +--- src/Avalonia.Controls/IMenu.cs | 3 +-- src/Avalonia.Controls/IMenuElement.cs | 3 +-- src/Avalonia.Controls/IMenuItem.cs | 4 +--- src/Avalonia.Controls/IScrollable.cs | 2 -- src/Avalonia.Controls/IVirtualizingPanel.cs | 2 +- src/Avalonia.Controls/ItemsControl.cs | 2 +- src/Avalonia.Controls/Menu.cs | 2 +- .../Mixins/ContentControlMixin.cs | 1 - .../NumericUpDown/NumericUpDown.cs | 8 +++---- .../ExportWindowingSubsystemAttribute.cs | 4 ---- .../Platform/IMenuInteractionHandler.cs | 5 +--- .../Platform/IPlatformIconLoader.cs | 8 +------ .../Platform/ITopLevelImpl.cs | 24 +++++++++---------- .../Platform/IWindowBaseImpl.cs | 6 ++--- .../Platform/IWindowingPlatform.cs | 6 ----- .../Surfaces/IFramebufferPlatformSurface.cs | 7 +----- .../Presenters/CarouselPresenter.cs | 3 +-- .../Presenters/IPresenter.cs | 1 - .../Presenters/ItemVirtualizer.cs | 1 - .../Presenters/ItemVirtualizerSimple.cs | 2 +- .../Presenters/ScrollContentPresenter.cs | 3 +-- .../Presenters/TextPresenter.cs | 3 +-- .../Primitives/HeaderedItemsControl.cs | 2 -- src/Avalonia.Controls/Primitives/Popup.cs | 5 ++-- src/Avalonia.Controls/Primitives/PopupRoot.cs | 3 --- src/Avalonia.Controls/Primitives/RangeBase.cs | 2 +- .../Primitives/SelectingItemsControl.cs | 1 - src/Avalonia.Controls/Primitives/Thumb.cs | 1 - src/Avalonia.Controls/ProgressBar.cs | 4 ---- src/Avalonia.Controls/Remote/RemoteServer.cs | 4 ---- src/Avalonia.Controls/Remote/RemoteWidget.cs | 3 +-- .../Remote/Server/RemoteServerTopLevelImpl.cs | 7 ++---- src/Avalonia.Controls/Separator.cs | 5 ---- src/Avalonia.Controls/Shapes/Path.cs | 1 - src/Avalonia.Controls/Shapes/Shape.cs | 1 - src/Avalonia.Controls/Spinner.cs | 4 +--- .../Templates/FuncDataTemplate`1.cs | 6 ++--- .../Templates/FuncMemberSelector.cs | 4 ++-- .../Templates/FuncTreeDataTemplate.cs | 2 +- .../Templates/FuncTreeDataTemplate`1.cs | 2 +- .../Templates/IDataTemplateHost.cs | 1 - .../Templates/IMemberSelector.cs | 4 ++-- .../Templates/ITreeDataTemplate.cs | 4 ++-- src/Avalonia.Controls/TextBlock.cs | 4 ++-- src/Avalonia.Controls/ToolTip.cs | 2 +- src/Avalonia.Controls/ToolTipService.cs | 4 ++-- src/Avalonia.Controls/TopLevel.cs | 4 +--- src/Avalonia.Controls/TreeView.cs | 3 +-- src/Avalonia.Controls/Utils/AncestorFinder.cs | 5 +--- src/Avalonia.Controls/Utils/GridLayout.cs | 2 +- .../Utils/IEnumerableUtils.cs | 1 - .../SelectingItemsControlSelectionAdapter.cs | 7 ++---- src/Avalonia.Controls/Window.cs | 10 ++++---- src/Avalonia.Controls/WindowBase.cs | 3 --- src/Avalonia.Controls/WindowIcon.cs | 9 ++----- src/Avalonia.Controls/WrapPanel.cs | 20 ++++++++-------- 85 files changed, 121 insertions(+), 262 deletions(-) diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 83763c0836..c92d5d7694 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -57,14 +57,14 @@ namespace Avalonia.Controls public Action AfterSetupCallback { get; private set; } = builder => { }; /// - /// Gets or sets a method to call before Startis called on the . + /// Gets or sets a method to call before Start is called on the . /// public Action BeforeStartCallback { get; private set; } = builder => { }; - protected AppBuilderBase(IRuntimePlatform platform, Action platformSevices) + protected AppBuilderBase(IRuntimePlatform platform, Action platformServices) { RuntimePlatform = platform; - RuntimePlatformServicesInitializer = () => platformSevices((TAppBuilder)this); + RuntimePlatformServicesInitializer = () => platformServices((TAppBuilder)this); } /// diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 499b65c5b7..4c549ac7d4 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -9,7 +9,6 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Input.Raw; -using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Threading; diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 96fb9be8ac..1bc402bc2f 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -352,8 +352,8 @@ namespace Avalonia.Controls private Func>> _asyncPopulator; private CancellationTokenSource _populationCancellationTokenSource; - private bool _itemTemplateIsFromValueMemeberBinding = true; - private bool _settingItemTemplateFromValueMemeberBinding; + private bool _itemTemplateIsFromValueMemberBinding = true; + private bool _settingItemTemplateFromValueMemberBinding; private object _selectedItem; private bool _isDropDownOpen; @@ -788,12 +788,12 @@ namespace Avalonia.Controls private void OnItemTemplatePropertyChanged(AvaloniaPropertyChangedEventArgs e) { - if (!_settingItemTemplateFromValueMemeberBinding) - _itemTemplateIsFromValueMemeberBinding = false; + if (!_settingItemTemplateFromValueMemberBinding) + _itemTemplateIsFromValueMemberBinding = false; } private void OnValueMemberBindingChanged(IBinding value) { - if(_itemTemplateIsFromValueMemeberBinding) + if(_itemTemplateIsFromValueMemberBinding) { var template = new FuncDataTemplate( @@ -805,9 +805,9 @@ namespace Avalonia.Controls return control; }); - _settingItemTemplateFromValueMemeberBinding = true; + _settingItemTemplateFromValueMemberBinding = true; ItemTemplate = template; - _settingItemTemplateFromValueMemeberBinding = false; + _settingItemTemplateFromValueMemberBinding = false; } } diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 0382c8d675..5f84421c64 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Avalonia; using Avalonia.Controls.Utils; using Avalonia.Layout; using Avalonia.Media; @@ -115,4 +114,4 @@ namespace Avalonia.Controls return LayoutHelper.ArrangeChild(Child, finalSize, Padding, BorderThickness); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 029e4dadc8..8f5a32634e 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -3,14 +3,14 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; -using System; -using System.Collections.ObjectModel; -using System.Diagnostics; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs index 0d48418683..5d883f2d14 100644 --- a/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs +++ b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs @@ -3,11 +3,10 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. -using Avalonia.Threading; using System; using System.Collections.ObjectModel; using System.Linq; -using System.Threading; +using Avalonia.Threading; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Calendar/CalendarDateRange.cs b/src/Avalonia.Controls/Calendar/CalendarDateRange.cs index 718cc7142b..47a0ad047b 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDateRange.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDateRange.cs @@ -4,7 +4,6 @@ // All other rights reserved. using System; -using System.Diagnostics; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index f6d0fbba62..1b36f92fd3 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -3,11 +3,9 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. -using Avalonia.Input; using System; -using System.Collections.Generic; using System.Globalization; -using System.Text; +using Avalonia.Input; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Calendar/CalendarExtensions.cs b/src/Avalonia.Controls/Calendar/CalendarExtensions.cs index 4fda02bff3..73de1c38f3 100644 --- a/src/Avalonia.Controls/Calendar/CalendarExtensions.cs +++ b/src/Avalonia.Controls/Calendar/CalendarExtensions.cs @@ -3,10 +3,7 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. -using System; -using System.Collections.Generic; using Avalonia.Input; -using System.Diagnostics; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index b0cbd0be53..577555333f 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -3,13 +3,13 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. +using System; +using System.Diagnostics; +using System.Globalization; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; -using System; -using System.Diagnostics; -using System.Globalization; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index 08608ad359..2270598623 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -3,15 +3,14 @@ // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. -using Avalonia.Controls.Primitives; -using Avalonia.Data; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Media; using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -1083,7 +1082,7 @@ namespace Avalonia.Controls else { // If parse error: TextBox should have the latest valid - // selecteddate value: + // SelectedDate value: if (SelectedDate != null) { string newtext = this.DateTimeToString(SelectedDate.Value); diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 0accb284b6..a69152c42b 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -34,7 +34,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - /// The menu iteraction handler. + /// The menu interaction handler. public ContextMenu(IMenuInteractionHandler interactionHandler) { Contract.Requires(interactionHandler != null); diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 67288972b6..a00d586233 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -1,22 +1,10 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Avalonia.Collections; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; -using Avalonia.Data; -using Avalonia.Diagnostics; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Logging; -using Avalonia.LogicalTree; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.VisualTree; @@ -134,9 +122,9 @@ namespace Avalonia.Controls } /// - /// Gets the element that recieves the focus adorner. + /// Gets the element that receives the focus adorner. /// - /// The control that recieves the focus adorner. + /// The control that receives the focus adorner. protected virtual IControl GetTemplateFocusTarget() { return this; diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs index a55bd63aa8..f0d7f8257e 100644 --- a/src/Avalonia.Controls/DataValidationErrors.cs +++ b/src/Avalonia.Controls/DataValidationErrors.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; -using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index c177d43917..224af979ab 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -1,7 +1,6 @@ using System; using Avalonia.Controls.Platform; using Avalonia.Input; -using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Styling; using JetBrains.Annotations; diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index 5becdc0f61..8b39cc03b8 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -1,9 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Layout; using Avalonia.Styling; namespace Avalonia.Controls.Embedding.Offscreen diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index c986c5d07c..37bb72e75a 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 5323939b50..1049d742f3 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -1,6 +1,5 @@ using Avalonia.Animation; using Avalonia.Controls.Primitives; -using Avalonia.VisualTree; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index d139c95fd4..653a4f5dcb 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls.Generators Type ContainerType { get; } /// - /// Signalled whenever new containers are materialized. + /// Signaled whenever new containers are materialized. /// event EventHandler Materialized; @@ -110,4 +110,4 @@ namespace Avalonia.Controls.Generators /// The index of the container, or -1 if not found. int IndexFromContainer(IControl container); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs index 224fc9d17e..e2e591215e 100644 --- a/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs @@ -1,8 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Collections.Generic; - namespace Avalonia.Controls.Generators { /// @@ -15,4 +13,4 @@ namespace Avalonia.Controls.Generators /// TreeContainerIndex Index { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 259c524d59..320d6c8faf 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq.Expressions; -using System.Reflection; using Avalonia.Controls.Templates; using Avalonia.Data; diff --git a/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs index c9b3a55aaa..d3cf70e0f8 100644 --- a/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Avalonia.Controls.Generators +namespace Avalonia.Controls.Generators { public class MenuItemContainerGenerator : ItemContainerGenerator { diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs index 08c11e2965..24b3fc1f32 100644 --- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs +++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls.Generators private readonly Dictionary _containerToItem = new Dictionary(); /// - /// Signalled whenever new containers are materialized. + /// Signaled whenever new containers are materialized. /// public event EventHandler Materialized; diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index abcecb5b82..304c86dbf7 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Controls.Templates; using Avalonia.Data; diff --git a/src/Avalonia.Controls/GridLength.cs b/src/Avalonia.Controls/GridLength.cs index 608879812c..f6a608cd71 100644 --- a/src/Avalonia.Controls/GridLength.cs +++ b/src/Avalonia.Controls/GridLength.cs @@ -1,11 +1,10 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Avalonia.Utilities; using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -218,4 +217,4 @@ namespace Avalonia.Controls } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 8112b2babd..1e4c6f2c2a 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -116,27 +116,27 @@ namespace Avalonia.Controls _orientation = DetectOrientation(); - int defenitionIndex; //row or col + int definitionIndex; //row or col if (_orientation == Orientation.Vertical) { Cursor = new Cursor(StandardCursorType.SizeWestEast); _definitions = _grid.ColumnDefinitions.Cast().ToList(); - defenitionIndex = GetValue(Grid.ColumnProperty); + definitionIndex = GetValue(Grid.ColumnProperty); PseudoClasses.Add(":vertical"); } else { Cursor = new Cursor(StandardCursorType.SizeNorthSouth); - defenitionIndex = GetValue(Grid.RowProperty); + definitionIndex = GetValue(Grid.RowProperty); _definitions = _grid.RowDefinitions.Cast().ToList(); PseudoClasses.Add(":horizontal"); } - if (defenitionIndex > 0) - _prevDefinition = _definitions[defenitionIndex - 1]; + if (definitionIndex > 0) + _prevDefinition = _definitions[definitionIndex - 1]; - if (defenitionIndex < _definitions.Count - 1) - _nextDefinition = _definitions[defenitionIndex + 1]; + if (definitionIndex < _definitions.Count - 1) + _nextDefinition = _definitions[definitionIndex + 1]; } private Orientation DetectOrientation() @@ -167,4 +167,4 @@ namespace Avalonia.Controls return Orientation.Vertical; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index a59fb86fb7..95752e7875 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -1,10 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Input; -using Avalonia.Controls; using Avalonia.Controls.Utils; using Avalonia.Input; diff --git a/src/Avalonia.Controls/IApplicationLifecycle.cs b/src/Avalonia.Controls/IApplicationLifecycle.cs index beb97a44ae..51f554c078 100644 --- a/src/Avalonia.Controls/IApplicationLifecycle.cs +++ b/src/Avalonia.Controls/IApplicationLifecycle.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/IControl.cs b/src/Avalonia.Controls/IControl.cs index e7f2903249..87b66d5f81 100644 --- a/src/Avalonia.Controls/IControl.cs +++ b/src/Avalonia.Controls/IControl.cs @@ -1,12 +1,9 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; -using Avalonia.LogicalTree; -using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -23,4 +20,4 @@ namespace Avalonia.Controls { new IControl Parent { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/IMenu.cs b/src/Avalonia.Controls/IMenu.cs index e118ec043c..0722a22f08 100644 --- a/src/Avalonia.Controls/IMenu.cs +++ b/src/Avalonia.Controls/IMenu.cs @@ -1,5 +1,4 @@ -using System; -using Avalonia.Controls.Platform; +using Avalonia.Controls.Platform; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/IMenuElement.cs b/src/Avalonia.Controls/IMenuElement.cs index c9fc04dcc8..ee9d0fd6b6 100644 --- a/src/Avalonia.Controls/IMenuElement.cs +++ b/src/Avalonia.Controls/IMenuElement.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Avalonia.Input; namespace Avalonia.Controls diff --git a/src/Avalonia.Controls/IMenuItem.cs b/src/Avalonia.Controls/IMenuItem.cs index 2657b1949f..132d565cb7 100644 --- a/src/Avalonia.Controls/IMenuItem.cs +++ b/src/Avalonia.Controls/IMenuItem.cs @@ -1,6 +1,4 @@ -using System; - -namespace Avalonia.Controls +namespace Avalonia.Controls { /// /// Represents a . diff --git a/src/Avalonia.Controls/IScrollable.cs b/src/Avalonia.Controls/IScrollable.cs index 9bbd0d8518..204e918d7b 100644 --- a/src/Avalonia.Controls/IScrollable.cs +++ b/src/Avalonia.Controls/IScrollable.cs @@ -1,8 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index 2d8dcb42e3..cba196dfa8 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls /// Gets or sets the controller for the virtualizing panel. /// /// - /// A virtualizing controller is responsible for maintaing the controls in the virtualizing + /// A virtualizing controller is responsible for maintaining the controls in the virtualizing /// panel. This property will be set by the controller when virtualization is initialized. /// Note that this property may remain null if the panel is added to a control that does /// not act as a virtualizing controller. diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 676e0af3de..9d4cbb9260 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -365,7 +365,7 @@ namespace Avalonia.Controls } /// - /// Caled when the property changes. + /// Called when the property changes. /// /// The event args. protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e) diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index edd7ed489e..00fca385a0 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -56,7 +56,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - /// The menu iteraction handler. + /// The menu interaction handler. public Menu(IMenuInteractionHandler interactionHandler) { Contract.Requires(interactionHandler != null); diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs index 95193c0432..6519fa4c14 100644 --- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs +++ b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs @@ -9,7 +9,6 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Styling; namespace Avalonia.Controls.Mixins { diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 59d2949b81..de68eb0ab0 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -526,7 +526,7 @@ namespace Avalonia.Controls return result; } - // Since the conversion from Value to text using a FormartString may not be parsable, + // Since the conversion from Value to text using a FormatString may not be parsable, // we verify that the already existing text is not the exact same value. var currentValueText = ConvertValueToText(); if (Equals(currentValueText, text)) @@ -571,7 +571,7 @@ namespace Avalonia.Controls } /// - /// Called by OnSpin when the spin direction is SpinDirection.Descrease. + /// Called by OnSpin when the spin direction is SpinDirection.Decrease. /// private void OnDecrement() { @@ -890,7 +890,7 @@ namespace Avalonia.Controls if (_isTextChangedFromUI && !parsedTextIsValid) { // Text input was made from the user and the text - // repesents an invalid value. Disable the spinner in this case. + // represents an invalid value. Disable the spinner in this case. if (Spinner != null) { Spinner.ValidSpinDirection = ValidSpinDirections.None; @@ -995,4 +995,4 @@ namespace Avalonia.Controls return false; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs b/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs index 420c56111f..e958b7aa15 100644 --- a/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs +++ b/src/Avalonia.Controls/Platform/ExportWindowingSubsystemAttribute.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Avalonia.Platform { diff --git a/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs index 342d3dd1c9..dd8503f768 100644 --- a/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs @@ -1,7 +1,4 @@ -using System; -using Avalonia.Input; - -namespace Avalonia.Controls.Platform +namespace Avalonia.Controls.Platform { /// /// Handles user interaction for menus. diff --git a/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs b/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs index a84a58906e..ecbc6d2234 100644 --- a/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs +++ b/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Media.Imaging; +using System.IO; namespace Avalonia.Platform { diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 60e25d2be6..ba022ab8d4 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -20,12 +20,12 @@ namespace Avalonia.Platform public interface ITopLevelImpl : IDisposable { /// - /// Gets the client size of the toplevel. + /// Gets the client size of the top level. /// Size ClientSize { get; } /// - /// Gets the scaling factor for the toplevel. + /// Gets the scaling factor for the top level. /// double Scaling { get; } @@ -42,38 +42,38 @@ namespace Avalonia.Platform IEnumerable Surfaces { get; } /// - /// Gets or sets a method called when the toplevel receives input. + /// Gets or sets a method called when the top level receives input. /// Action Input { get; set; } /// - /// Gets or sets a method called when the toplevel requires painting. + /// Gets or sets a method called when the top level requires painting. /// Action Paint { get; set; } /// - /// Gets or sets a method called when the toplevel is resized. + /// Gets or sets a method called when the top level is resized. /// Action Resized { get; set; } /// - /// Gets or sets a method called when the toplevel's scaling changes. + /// Gets or sets a method called when the top level's scaling changes. /// Action ScalingChanged { get; set; } /// - /// Creates a new renderer for the toplevel. + /// Creates a new renderer for the top level. /// - /// The toplevel. + /// The top level. IRenderer CreateRenderer(IRenderRoot root); /// - /// Invalidates a rect on the toplevel. + /// Invalidates a rect on the top level. /// void Invalidate(Rect rect); /// - /// Sets the for the toplevel. + /// Sets the for the top level. /// void SetInputRoot(IInputRoot inputRoot); @@ -92,7 +92,7 @@ namespace Avalonia.Platform Point PointToScreen(Point point); /// - /// Sets the cursor associated with the toplevel. + /// Sets the cursor associated with the top level. /// /// The cursor. Use null for default cursor void SetCursor(IPlatformHandle cursor); @@ -103,7 +103,7 @@ namespace Avalonia.Platform Action Closed { get; set; } /// - /// Gets a mouse device associated with toplevel + /// Gets a mouse device associated with top level /// [CanBeNull] IMouseDevice MouseDevice { get; } diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index 9ba68f584e..e788b3c73e 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -6,7 +6,7 @@ namespace Avalonia.Platform public interface IWindowBaseImpl : ITopLevelImpl { /// - /// Shows the toplevel. + /// Shows the top level. /// void Show(); @@ -62,7 +62,7 @@ namespace Avalonia.Platform Size MaxClientSize { get; } /// - /// Sets the client size of the toplevel. + /// Sets the client size of the top level. /// void Resize(Size clientSize); @@ -82,4 +82,4 @@ namespace Avalonia.Platform /// IScreenImpl Screen { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index 5dcd0a39e8..5c2c1a8da3 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Avalonia.Platform { public interface IWindowingPlatform diff --git a/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs b/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs index 4dc96a074d..62cd012d51 100644 --- a/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs +++ b/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Platform; +using Avalonia.Platform; namespace Avalonia.Controls.Platform.Surfaces { diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 1d5a187a73..ba2c7c91d8 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Animation; -using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Data; @@ -251,4 +250,4 @@ namespace Avalonia.Controls.Presenters } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Presenters/IPresenter.cs b/src/Avalonia.Controls/Presenters/IPresenter.cs index e06be4e82b..f78ba4c19d 100644 --- a/src/Avalonia.Controls/Presenters/IPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IPresenter.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Controls.Primitives; -using Avalonia.Styling; namespace Avalonia.Controls.Presenters { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index e293cff211..c5344b29d9 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -8,7 +8,6 @@ using System.Reactive.Linq; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; -using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index b98f26b87f..f31a48b2a0 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -390,7 +390,7 @@ namespace Avalonia.Controls.Presenters /// The delta of the move. /// /// If the move is less than a page, then this method moves the containers for the items - /// that are still visible to the correct place, and recyles and moves the others. For + /// that are still visible to the correct place, and recycles and moves the others. For /// example: if there are 20 items and 10 containers visible and the user scrolls 5 /// items down, then the bottom 5 containers will be moved to the top and the top 5 will /// be moved to the bottom and recycled to display the newly visible item. Updates diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 8d0c6f16cb..2ef7941b55 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -7,7 +7,6 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.Layout; using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters @@ -319,4 +318,4 @@ namespace Avalonia.Controls.Presenters } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 636c836da5..3181e76c3f 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -4,7 +4,6 @@ using System; using System.Reactive.Linq; using Avalonia.Media; -using Avalonia.Styling; using Avalonia.Threading; using Avalonia.VisualTree; @@ -116,7 +115,7 @@ namespace Avalonia.Controls.Presenters var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; - // issue #600: set constaint before any FormattedText manipulation + // issue #600: set constraint before any FormattedText manipulation // see base.Render(...) implementation FormattedText.Constraint = Bounds.Size; diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index 82ab929d53..c5aa73e56a 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -1,8 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // 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.Mixins; using Avalonia.Controls.Presenters; diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 005717d681..5e79711f5a 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -6,11 +6,10 @@ using System.Linq; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; -using Avalonia.Rendering; using Avalonia.VisualTree; -using Avalonia.Layout; namespace Avalonia.Controls.Primitives { @@ -473,4 +472,4 @@ namespace Avalonia.Controls.Primitives } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 0ae4be5550..fdec9febd3 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -2,12 +2,9 @@ // 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.Platform; using Avalonia.Controls.Presenters; using Avalonia.Interactivity; -using Avalonia.Layout; -using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Styling; diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index b46562e99e..76df94cdb8 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -130,7 +130,7 @@ namespace Avalonia.Controls.Primitives } /// - /// Throws an exception if the double valus is NaN or Inf. + /// Throws an exception if the double value is NaN or Inf. /// /// The value. /// The name of the property being set. diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 5451cf0701..bb39b005cc 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -11,7 +11,6 @@ using Avalonia.Controls.Generators; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Metadata; using Avalonia.Styling; using Avalonia.VisualTree; diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index b0f9ab1f85..b01ddd5dba 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -4,7 +4,6 @@ using System; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Layout; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index b7db352c74..085db75ce1 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -1,12 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Reactive.Linq; -using Avalonia.Animation; using Avalonia.Controls.Primitives; -using Avalonia.Layout; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/Remote/RemoteServer.cs b/src/Avalonia.Controls/Remote/RemoteServer.cs index 9c0f3464a5..e116316904 100644 --- a/src/Avalonia.Controls/Remote/RemoteServer.cs +++ b/src/Avalonia.Controls/Remote/RemoteServer.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Controls.Embedding; using Avalonia.Controls.Remote.Server; using Avalonia.Platform; diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index ea8c3ebe52..2d4f2e6b52 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.InteropServices; -using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Remote.Protocol; @@ -76,4 +75,4 @@ namespace Avalonia.Controls.Remote base.Render(context); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index cf4cec9268..b302f2f5ec 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; using Avalonia.Controls.Embedding.Offscreen; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; @@ -94,10 +91,10 @@ namespace Avalonia.Controls.Remote.Server RenderIfNeeded(); } - protected virtual Size Measure(Size constaint) + protected virtual Size Measure(Size constraint) { var l = (ILayoutable) InputRoot; - l.Measure(constaint); + l.Measure(constraint); return l.DesiredSize; } diff --git a/src/Avalonia.Controls/Separator.cs b/src/Avalonia.Controls/Separator.cs index 2028a5cfbb..84b2a33d7b 100644 --- a/src/Avalonia.Controls/Separator.cs +++ b/src/Avalonia.Controls/Separator.cs @@ -1,11 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Controls.Primitives; namespace Avalonia.Controls diff --git a/src/Avalonia.Controls/Shapes/Path.cs b/src/Avalonia.Controls/Shapes/Path.cs index 08bed79b3a..e0952d3e9b 100644 --- a/src/Avalonia.Controls/Shapes/Path.cs +++ b/src/Avalonia.Controls/Shapes/Path.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; using Avalonia.Media; namespace Avalonia.Controls.Shapes diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index a1848a95b1..604051ef28 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -4,7 +4,6 @@ using System; using System.Reflection; using Avalonia.Collections; -using Avalonia.Controls; using Avalonia.Media; namespace Avalonia.Controls.Shapes diff --git a/src/Avalonia.Controls/Spinner.cs b/src/Avalonia.Controls/Spinner.cs index e00ff3823c..5ee13f45c4 100644 --- a/src/Avalonia.Controls/Spinner.cs +++ b/src/Avalonia.Controls/Spinner.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Avalonia.Interactivity; namespace Avalonia.Controls @@ -54,7 +52,7 @@ namespace Avalonia.Controls public SpinDirection Direction { get; } /// - /// Get or set whheter the spin event originated from a mouse wheel event. + /// Get or set whether the spin event originated from a mouse wheel event. /// public bool UsingMouseWheel{ get; } diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs index 7154c0e558..9339aa6924 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Templates } /// - /// Casts a stongly typed match function to a weakly typed one. + /// Casts a strongly typed match function to a weakly typed one. /// /// The strongly typed function. /// The weakly typed function. @@ -52,7 +52,7 @@ namespace Avalonia.Controls.Templates } /// - /// Casts a stongly typed build function to a weakly typed one. + /// Casts a strongly typed build function to a weakly typed one. /// /// The strong data type. /// The strongly typed function. @@ -62,4 +62,4 @@ namespace Avalonia.Controls.Templates return o => f((T)o); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs b/src/Avalonia.Controls/Templates/FuncMemberSelector.cs index b8f2c2e311..5ab186261e 100644 --- a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs +++ b/src/Avalonia.Controls/Templates/FuncMemberSelector.cs @@ -25,11 +25,11 @@ namespace Avalonia.Controls.Templates /// /// Selects a member of an object. /// - /// The obeject. + /// The object. /// The selected member. public object Select(object o) { return (o is TObject) ? _selector((TObject)o) : default(TMember); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs index 2e55dd3552..e7c9cf8608 100644 --- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs @@ -9,7 +9,7 @@ using Avalonia.Data; namespace Avalonia.Controls.Templates { /// - /// A template used to build hierachical data. + /// A template used to build hierarchical data. /// public class FuncTreeDataTemplate : FuncDataTemplate, ITreeDataTemplate { diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs index d7d6a87769..4ca96f60bd 100644 --- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs @@ -7,7 +7,7 @@ using System.Collections; namespace Avalonia.Controls.Templates { /// - /// A template used to build hierachical data. + /// A template used to build hierarchical data. /// /// The type of the template's data. public class FuncTreeDataTemplate : FuncTreeDataTemplate diff --git a/src/Avalonia.Controls/Templates/IDataTemplateHost.cs b/src/Avalonia.Controls/Templates/IDataTemplateHost.cs index 5cc12581d4..267938b3bd 100644 --- a/src/Avalonia.Controls/Templates/IDataTemplateHost.cs +++ b/src/Avalonia.Controls/Templates/IDataTemplateHost.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; namespace Avalonia.Controls.Templates { diff --git a/src/Avalonia.Controls/Templates/IMemberSelector.cs b/src/Avalonia.Controls/Templates/IMemberSelector.cs index b7172fa492..e1ec42a849 100644 --- a/src/Avalonia.Controls/Templates/IMemberSelector.cs +++ b/src/Avalonia.Controls/Templates/IMemberSelector.cs @@ -11,8 +11,8 @@ namespace Avalonia.Controls.Templates /// /// Selects a member of an object. /// - /// The obeject. + /// The object. /// The selected member. object Select(object o); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/ITreeDataTemplate.cs b/src/Avalonia.Controls/Templates/ITreeDataTemplate.cs index 580839be97..9ed8987681 100644 --- a/src/Avalonia.Controls/Templates/ITreeDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/ITreeDataTemplate.cs @@ -6,7 +6,7 @@ using Avalonia.Data; namespace Avalonia.Controls.Templates { /// - /// Interface representing a template used to build hierachical data. + /// Interface representing a template used to build hierarchical data. /// public interface ITreeDataTemplate : IDataTemplate { @@ -20,4 +20,4 @@ namespace Avalonia.Controls.Templates /// InstancedBinding ItemsSelector(object item); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index fd46efa76f..e91d2e8fa7 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -21,7 +21,7 @@ namespace Avalonia.Controls public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); - // TODO: Define these attached properties elswhere (e.g. on a Text class) and AddOwner + // TODO: Define these attached properties elsewhere (e.g. on a Text class) and AddOwner // them into TextBlock. /// @@ -406,4 +406,4 @@ namespace Avalonia.Controls InvalidateMeasure(); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 10e964d014..28d1ba5e0f 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached("ShowDelay", 400); /// - /// Stores the curernt instance in the control. + /// Stores the current instance in the control. /// private static readonly AttachedProperty ToolTipProperty = AvaloniaProperty.RegisterAttached("ToolTip"); diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index bfd7ef0f33..384a9db0cf 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -5,7 +5,7 @@ using Avalonia.Threading; namespace Avalonia.Controls { /// - /// Handeles interaction with controls. + /// Handles interaction with controls. /// internal sealed class ToolTipService { @@ -95,4 +95,4 @@ namespace Avalonia.Controls _timer = null; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 9fdc097c3f..1161ded25f 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Disposables; using System.Reactive.Linq; -using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; @@ -225,7 +223,7 @@ namespace Avalonia.Controls protected virtual IRenderTarget CreateRenderTarget() { if(PlatformImpl == null) - throw new InvalidOperationException("Cann't create render target, PlatformImpl is null (might be already disposed)"); + throw new InvalidOperationException("Can't create render target, PlatformImpl is null (might be already disposed)"); return _renderInterface.CreateRenderTarget(PlatformImpl.Surfaces); } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 4575fa767b..3003dab85e 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // 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.Primitives; @@ -14,7 +13,7 @@ using Avalonia.VisualTree; namespace Avalonia.Controls { /// - /// Displays a hierachical tree of data. + /// Displays a hierarchical tree of data. /// public class TreeView : ItemsControl, ICustomKeyboardNavigation { diff --git a/src/Avalonia.Controls/Utils/AncestorFinder.cs b/src/Avalonia.Controls/Utils/AncestorFinder.cs index 6ee284e05d..d02202ca02 100644 --- a/src/Avalonia.Controls/Utils/AncestorFinder.cs +++ b/src/Avalonia.Controls/Utils/AncestorFinder.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; +using System; using System.Linq; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace Avalonia.Controls.Utils { diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 10a94a8c82..363428b289 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -376,7 +376,7 @@ namespace Avalonia.Controls.Utils // 1. Determine all one-span column's desired widths or row's desired heights. // 2. Order the multi-span conventions by its last index // (Notice that the sorted data is much smaller than the source.) - // 3. Determine each multi-span last index by calculating the maximun desired size. + // 3. Determine each multi-span last index by calculating the maximum desired size. // Before we determine the behavior of this method, we just aggregate the one-span * columns. diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs index 361857aeb7..40ebd406c3 100644 --- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs +++ b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs @@ -3,7 +3,6 @@ using System; using System.Collections; -using System.Globalization; using System.Linq; namespace Avalonia.Controls.Utils diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs index 43c8a5aa6c..4d814170c6 100644 --- a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs +++ b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs @@ -4,15 +4,12 @@ // All other rights reserved. using System; +using System.Collections; using System.Linq; -using System.Collections.Generic; -using System.Text; using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.LogicalTree; -using System.Collections; -using System.Diagnostics; namespace Avalonia.Controls.Utils { diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index bd07cf6740..39b4f05545 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -86,7 +86,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(Icon)); /// - /// Defines the proeprty. + /// Defines the property. /// public static readonly DirectProperty WindowStartupLocationProperty = AvaloniaProperty.RegisterDirect( @@ -100,7 +100,7 @@ namespace Avalonia.Controls private readonly NameScope _nameScope = new NameScope(); private object _dialogResult; private readonly Size _maxPlatformClientSize; - private WindowStartupLocation _windowStartupLoction; + private WindowStartupLocation _windowStartupLocation; /// /// Initializes static members of the class. @@ -237,8 +237,8 @@ namespace Avalonia.Controls /// public WindowStartupLocation WindowStartupLocation { - get { return _windowStartupLoction; } - set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLoction, value); } + get { return _windowStartupLocation; } + set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLocation, value); } } /// @@ -409,7 +409,7 @@ namespace Avalonia.Controls /// The type of the result produced by the dialog. /// /// . - /// A task that can be used to retrive the result of the dialog when it closes. + /// A task that can be used to retrieve the result of the dialog when it closes. /// public Task ShowDialog() { diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index c0b664ebc3..f609432545 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -1,10 +1,7 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; diff --git a/src/Avalonia.Controls/WindowIcon.cs b/src/Avalonia.Controls/WindowIcon.cs index 26195e6d76..9e4329372a 100644 --- a/src/Avalonia.Controls/WindowIcon.cs +++ b/src/Avalonia.Controls/WindowIcon.cs @@ -1,11 +1,6 @@ -using Avalonia.Platform; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.IO; using Avalonia.Media.Imaging; +using Avalonia.Platform; namespace Avalonia.Controls { diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 84d3cc791e..8ee0636124 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -132,7 +132,7 @@ namespace Avalonia.Controls double accumulatedV = 0; var uvFinalSize = CreateUVSize(finalSize); var lineSize = CreateUVSize(); - int firstChildInLineindex = 0; + int firstChildInLineIndex = 0; for (int index = 0; index < Children.Count; index++) { var child = Children[index]; @@ -144,32 +144,32 @@ namespace Avalonia.Controls } else // moving to next line { - var controlsInLine = GetContolsBetween(firstChildInLineindex, index); + var controlsInLine = GetControlsBetween(firstChildInLineIndex, index); ArrangeLine(accumulatedV, lineSize.V, controlsInLine); accumulatedV += lineSize.V; lineSize = childSize; - firstChildInLineindex = index; + firstChildInLineIndex = index; } } - if (firstChildInLineindex < Children.Count) + if (firstChildInLineIndex < Children.Count) { - var controlsInLine = GetContolsBetween(firstChildInLineindex, Children.Count); + var controlsInLine = GetControlsBetween(firstChildInLineIndex, Children.Count); ArrangeLine(accumulatedV, lineSize.V, controlsInLine); } return finalSize; } - private IEnumerable GetContolsBetween(int first, int last) + private IEnumerable GetControlsBetween(int first, int last) { return Children.Skip(first).Take(last - first); } - private void ArrangeLine(double v, double lineV, IEnumerable contols) + private void ArrangeLine(double v, double lineV, IEnumerable controls) { double u = 0; bool isHorizontal = (Orientation == Orientation.Horizontal); - foreach (var child in contols) + foreach (var child in controls) { var childSize = CreateUVSize(child.DesiredSize); var x = isHorizontal ? u : v; @@ -181,9 +181,9 @@ namespace Avalonia.Controls } } /// - /// Used to not not write sepearate code for horizontal and vertical orientation. + /// Used to not not write separate code for horizontal and vertical orientation. /// U is direction in line. (x if orientation is horizontal) - /// V is direction of lines. (y if orientation is horizonral) + /// V is direction of lines. (y if orientation is horizontal) /// [DebuggerDisplay("U = {U} V = {V}")] private struct UVSize From 47b78a413b842b2f46c2eba7d8ba10fe520382f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Marki=C4=8D?= Date: Sun, 2 Sep 2018 09:16:10 +0200 Subject: [PATCH 19/23] Reverts 'top level' comment back to 'toplevel' --- .../Platform/ITopLevelImpl.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index ba022ab8d4..60e25d2be6 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -20,12 +20,12 @@ namespace Avalonia.Platform public interface ITopLevelImpl : IDisposable { /// - /// Gets the client size of the top level. + /// Gets the client size of the toplevel. /// Size ClientSize { get; } /// - /// Gets the scaling factor for the top level. + /// Gets the scaling factor for the toplevel. /// double Scaling { get; } @@ -42,38 +42,38 @@ namespace Avalonia.Platform IEnumerable Surfaces { get; } /// - /// Gets or sets a method called when the top level receives input. + /// Gets or sets a method called when the toplevel receives input. /// Action Input { get; set; } /// - /// Gets or sets a method called when the top level requires painting. + /// Gets or sets a method called when the toplevel requires painting. /// Action Paint { get; set; } /// - /// Gets or sets a method called when the top level is resized. + /// Gets or sets a method called when the toplevel is resized. /// Action Resized { get; set; } /// - /// Gets or sets a method called when the top level's scaling changes. + /// Gets or sets a method called when the toplevel's scaling changes. /// Action ScalingChanged { get; set; } /// - /// Creates a new renderer for the top level. + /// Creates a new renderer for the toplevel. /// - /// The top level. + /// The toplevel. IRenderer CreateRenderer(IRenderRoot root); /// - /// Invalidates a rect on the top level. + /// Invalidates a rect on the toplevel. /// void Invalidate(Rect rect); /// - /// Sets the for the top level. + /// Sets the for the toplevel. /// void SetInputRoot(IInputRoot inputRoot); @@ -92,7 +92,7 @@ namespace Avalonia.Platform Point PointToScreen(Point point); /// - /// Sets the cursor associated with the top level. + /// Sets the cursor associated with the toplevel. /// /// The cursor. Use null for default cursor void SetCursor(IPlatformHandle cursor); @@ -103,7 +103,7 @@ namespace Avalonia.Platform Action Closed { get; set; } /// - /// Gets a mouse device associated with top level + /// Gets a mouse device associated with toplevel /// [CanBeNull] IMouseDevice MouseDevice { get; } From 01e1835ad884b4c85cca5f31c167876715099032 Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Sun, 2 Sep 2018 23:03:21 +0200 Subject: [PATCH 20/23] Corrected test implementation --- .../Mixins/ContentControlMixinTests.cs | 60 ++++++------------- 1 file changed, 18 insertions(+), 42 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs index a0487842a9..71c396b2c6 100644 --- a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections.Generic; +using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; @@ -41,20 +42,10 @@ namespace Avalonia.Controls.UnitTests.Mixins var p1 = new ContentPresenter { Name = "Content_1_Presenter" }; var p2 = new ContentPresenter { Name = "Content_2_Presenter" }; - var callIndex = -1; - var called = new bool[4]; + var itemsAddedThroughMixin = new List(); + var itemsNotAddedThroughMixin = new List(); - void Callback() - { - if (callIndex >= 0) - called[callIndex] = true; - } - - var listMock = new Mock>(); - listMock.Setup(l => l.Contains(It.IsAny())).Returns(false).Callback(Callback); - var list = listMock.Object; - - var target = new TestControl(list) + var target = new TestControl { Template = new FuncControlTemplate(_ => new Panel { @@ -67,26 +58,28 @@ namespace Avalonia.Controls.UnitTests.Mixins }; target.ApplyTemplate(); - callIndex = 0; - p1.Content = new Control(); + Control tc; + + p1.Content = tc = new Control(); p1.UpdateChild(); + itemsAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); - callIndex = 1; - p2.Content = new Control(); + p2.Content = tc = new Control(); p2.UpdateChild(); + itemsAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); target.Template = null; - callIndex = 2; - p1.Content = new Control(); + p1.Content = tc = new Control(); p1.UpdateChild(); + itemsNotAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); - callIndex = 3; - p2.Content = new Control(); + p2.Content = tc = new Control(); p2.UpdateChild(); + itemsNotAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); - - Assert.Equal(new[] { true, true, false, false }, called); + Assert.Equal(new[] { true, true }, itemsAddedThroughMixin); + Assert.Equal(new[] { false, false }, itemsNotAddedThroughMixin); } private class TestControl : TemplatedControl @@ -97,27 +90,10 @@ namespace Avalonia.Controls.UnitTests.Mixins public static readonly StyledProperty Content2Property = AvaloniaProperty.Register(nameof(Content2)); - static TestControl() { - ContentControlMixin.Attach(Content1Property, x => x.GetLogicalChildren(), "Content_1_Presenter"); - ContentControlMixin.Attach(Content2Property, x => x.GetLogicalChildren(), "Content_2_Presenter"); - } - - private IAvaloniaList _mock; - - public TestControl() - { - } - - public TestControl(IAvaloniaList mock) - { - _mock = mock; - } - - public IAvaloniaList GetLogicalChildren() - { - return _mock ?? LogicalChildren; + ContentControlMixin.Attach(Content1Property, x => x.LogicalChildren, "Content_1_Presenter"); + ContentControlMixin.Attach(Content2Property, x => x.LogicalChildren, "Content_2_Presenter"); } public object Content1 From 1b82998775a597e6896eba1b66a0ad6be5c35cfa Mon Sep 17 00:00:00 2001 From: wojciech krysiak Date: Sun, 2 Sep 2018 23:24:50 +0200 Subject: [PATCH 21/23] Made test more readable --- .../Mixins/ContentControlMixinTests.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs index 71c396b2c6..f06553411c 100644 --- a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs @@ -41,10 +41,6 @@ namespace Avalonia.Controls.UnitTests.Mixins { var p1 = new ContentPresenter { Name = "Content_1_Presenter" }; var p2 = new ContentPresenter { Name = "Content_2_Presenter" }; - - var itemsAddedThroughMixin = new List(); - var itemsNotAddedThroughMixin = new List(); - var target = new TestControl { Template = new FuncControlTemplate(_ => new Panel @@ -62,24 +58,22 @@ namespace Avalonia.Controls.UnitTests.Mixins p1.Content = tc = new Control(); p1.UpdateChild(); - itemsAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); + Assert.Contains(tc, target.GetLogicalChildren()); p2.Content = tc = new Control(); p2.UpdateChild(); - itemsAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); + Assert.Contains(tc, target.GetLogicalChildren()); target.Template = null; p1.Content = tc = new Control(); p1.UpdateChild(); - itemsNotAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); + Assert.DoesNotContain(tc, target.GetLogicalChildren()); p2.Content = tc = new Control(); p2.UpdateChild(); - itemsNotAddedThroughMixin.Add(target.GetLogicalChildren().Contains(tc)); + Assert.DoesNotContain(tc, target.GetLogicalChildren()); - Assert.Equal(new[] { true, true }, itemsAddedThroughMixin); - Assert.Equal(new[] { false, false }, itemsNotAddedThroughMixin); } private class TestControl : TemplatedControl From 158d2d31b3d6d193e22ff7031eeb0910bf242116 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 2 Sep 2018 23:54:46 +0200 Subject: [PATCH 22/23] Add RoutedEventRegistry. Fixes #1846. --- src/Avalonia.Interactivity/RoutedEvent.cs | 8 +- .../RoutedEventRegistry.cs | 90 +++++++++++++++++++ .../RoutedEventRegistryTests.cs | 49 ++++++++++ 3 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Interactivity/RoutedEventRegistry.cs create mode 100644 tests/Avalonia.Interactivity.UnitTests/RoutedEventRegistryTests.cs diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index 61bb567d54..2d752133c1 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -72,7 +72,9 @@ namespace Avalonia.Interactivity { Contract.Requires(name != null); - return new RoutedEvent(name, routingStrategy, typeof(TOwner)); + var routedEvent = new RoutedEvent(name, routingStrategy, typeof(TOwner)); + RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent); + return routedEvent; } public static RoutedEvent Register( @@ -83,7 +85,9 @@ namespace Avalonia.Interactivity { Contract.Requires(name != null); - return new RoutedEvent(name, routingStrategy, ownerType); + var routedEvent = new RoutedEvent(name, routingStrategy, ownerType); + RoutedEventRegistry.Instance.Register(ownerType, routedEvent); + return routedEvent; } public IDisposable AddClassHandler( diff --git a/src/Avalonia.Interactivity/RoutedEventRegistry.cs b/src/Avalonia.Interactivity/RoutedEventRegistry.cs new file mode 100644 index 0000000000..34c970a806 --- /dev/null +++ b/src/Avalonia.Interactivity/RoutedEventRegistry.cs @@ -0,0 +1,90 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace Avalonia.Interactivity +{ + /// + /// Tracks registered s. + /// + public class RoutedEventRegistry + { + private readonly Dictionary> _registeredRoutedEvents = + new Dictionary>(); + + /// + /// Gets the instance. + /// + public static RoutedEventRegistry Instance { get; } + = new RoutedEventRegistry(); + + /// + /// Registers a on a type. + /// + /// The type. + /// The event. + /// + /// You won't usually want to call this method directly, instead use the + /// + /// method. + /// + public void Register(Type type, RoutedEvent @event) + { + Contract.Requires(type != null); + Contract.Requires(@event != null); + + if (!_registeredRoutedEvents.TryGetValue(type, out var list)) + { + list = new List(); + _registeredRoutedEvents.Add(type, list); + } + list.Add(@event); + } + + /// + /// Returns all routed events, that are currently registered in the event registry. + /// + /// All routed events, that are currently registered in the event registry. + public IEnumerable GetAllRegistered() + { + foreach (var events in _registeredRoutedEvents.Values) + { + foreach (var e in events) + { + yield return e; + } + } + } + + /// + /// Returns all routed events registered with the provided type. + /// If the type is not found or does not provide any routed events, an empty list is returned. + /// + /// The type. + /// All routed events registered with the provided type. + public IReadOnlyList GetRegistered(Type type) + { + Contract.Requires(type != null); + + if (_registeredRoutedEvents.TryGetValue(type, out var events)) + { + return events; + } + + return Array.Empty(); + } + + /// + /// Returns all routed events registered with the provided type. + /// If the type is not found or does not provide any routed events, an empty list is returned. + /// + /// The type. + /// All routed events registered with the provided type. + public IReadOnlyList GetRegistered() + { + return GetRegistered(typeof(TOwner)); + } + } +} diff --git a/tests/Avalonia.Interactivity.UnitTests/RoutedEventRegistryTests.cs b/tests/Avalonia.Interactivity.UnitTests/RoutedEventRegistryTests.cs new file mode 100644 index 0000000000..b9ebdea064 --- /dev/null +++ b/tests/Avalonia.Interactivity.UnitTests/RoutedEventRegistryTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Input; +using Xunit; + +namespace Avalonia.Interactivity.UnitTests +{ + public class RoutedEventRegistryTests + { + [Fact] + public void Pointer_Events_Should_Be_Registered() + { + var expectedEvents = new List { InputElement.PointerPressedEvent, InputElement.PointerReleasedEvent }; + var registeredEvents = RoutedEventRegistry.Instance.GetRegistered(); + Assert.Contains(registeredEvents, expectedEvents.Contains); + } + + [Fact] + public void ClickEvent_Should_Be_Registered_On_Button() + { + var expectedEvents = new List { Button.ClickEvent }; + var registeredEvents = RoutedEventRegistry.Instance.GetRegistered