diff --git a/src/Avalonia.Controls/Canvas.cs b/src/Avalonia.Controls/Canvas.cs
index 8a80d6bdf74..5c9a97cb27f 100644
--- a/src/Avalonia.Controls/Canvas.cs
+++ b/src/Avalonia.Controls/Canvas.cs
@@ -136,8 +136,9 @@ public static void SetBottom(AvaloniaObject element, double value)
///
/// 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/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index 13f00bdc87a..0accb284b6f 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 @@ public class ContextMenu : SelectingItemsControl
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 @@ public class ContextMenu : SelectingItemsControl
static ContextMenu()
{
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
-
- MenuItem.ClickEvent.AddClassHandler(x => x.OnContextMenuClick, handledEventsToo: true);
}
///
@@ -36,6 +55,36 @@ static ContextMenu()
///
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 @@ static ContextMenu()
///
public event CancelEventHandler ContextMenuClosing;
-
///
/// Called when the property changes on a control.
///
@@ -71,62 +119,53 @@ private static void ContextMenuChanged(AvaloniaPropertyChangedEventArgs e)
}
///
- /// 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 @@ private static void ControlPointerReleased(object sender, PointerReleasedEventAr
if (contextMenu.CancelClosing())
return;
- control.ContextMenu.Hide();
+ control.ContextMenu.Close();
e.Handled = true;
}
@@ -161,7 +200,7 @@ private static void ControlPointerReleased(object sender, PointerReleasedEventAr
if (contextMenu.CancelOpening())
return;
- contextMenu.Show(control);
+ contextMenu.Open(control);
e.Handled = true;
}
}
@@ -179,5 +218,10 @@ private bool CancelOpening()
ContextMenuOpening?.Invoke(this, eventArgs);
return eventArgs.Cancel;
}
+
+ bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap)
+ {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/src/Avalonia.Controls/IMenu.cs b/src/Avalonia.Controls/IMenu.cs
new file mode 100644
index 00000000000..e118ec043cb
--- /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 00000000000..c9fc04dcc8b
--- /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 00000000000..2657b1949fb
--- /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 3cb997f6157..676e0af3de3 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -15,6 +15,7 @@
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
+using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@@ -323,6 +324,46 @@ protected virtual void OnContainersRecycled(ItemContainerEventArgs e)
LogicalChildren.RemoveAll(toRemove);
}
+ ///
+ /// Handles directional navigation within the .
+ ///
+ /// The key events.
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ if (!e.Handled)
+ {
+ 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;
+ }
+
+ var current = focus.Current
+ .GetSelfAndVisualAncestors()
+ .OfType()
+ .FirstOrDefault(x => x.VisualParent == container);
+
+ if (current != null)
+ {
+ var next = GetNextControl(container, direction.Value, current, false);
+
+ if (next != null)
+ {
+ focus.Focus(next, NavigationMethod.Directional);
+ e.Handled = true;
+ }
+ }
+ }
+
+ base.OnKeyDown(e);
+ }
+
///
/// Caled when the property changes.
///
@@ -335,6 +376,7 @@ protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
var oldValue = e.OldValue as IEnumerable;
var newValue = e.NewValue as IEnumerable;
+ UpdateItemCount();
RemoveControlItemsFromLogicalChildren(oldValue);
AddControlItemsToLogicalChildren(newValue);
SubscribeToItems(newValue);
@@ -358,10 +400,8 @@ protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionCha
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);
@@ -445,5 +485,44 @@ private void ItemTemplateChanged(AvaloniaPropertyChangedEventArgs e)
// 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();
+ }
+ }
+
+ protected static IInputElement GetNextControl(
+ INavigableContainer container,
+ NavigationDirection direction,
+ IInputElement from,
+ bool wrap)
+ {
+ IInputElement result;
+
+ do
+ {
+ result = container.GetControl(direction, from, wrap);
+
+ if (result?.Focusable == true)
+ {
+ return result;
+ }
+
+ from = result;
+ } while (from != null);
+
+ return null;
+ }
}
}
diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs
index 994af9dab8a..edd7ed489e5 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 @@ public class Menu : SelectingItemsControl, IFocusScope, IMainMenu
nameof(IsOpen),
o => o.IsOpen);
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent MenuOpenedEvent =
+ RoutedEvent.Register