Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Menu navigation #1812

Merged
merged 14 commits into from
Aug 26, 2018
Merged
3 changes: 2 additions & 1 deletion src/Avalonia.Controls/Canvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,9 @@ public static void SetBottom(AvaloniaObject element, double value)
/// </summary>
/// <param name="direction">The movement direction.</param>
/// <param name="from">The control from which movement begins.</param>
/// <param name="wrap">Whether to wrap around when the first or last item is reached.</param>
/// <returns>The control.</returns>
IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from)
IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from, bool wrap)
{
// TODO: Implement this
return null;
Expand Down
148 changes: 96 additions & 52 deletions src/Avalonia.Controls/ContextMenu.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -20,22 +22,69 @@ public class ContextMenu : SelectingItemsControl
public static readonly DirectProperty<ContextMenu, bool> IsOpenProperty =
AvaloniaProperty.RegisterDirect<ContextMenu, bool>(nameof(IsOpen), o => o.IsOpen);

/// <summary>
/// Initializes a new instance of the <see cref="ContextMenu"/> class.
/// </summary>
public ContextMenu()
{
_interaction = AvaloniaLocator.Current.GetService<IMenuInteractionHandler>() ??
new DefaultMenuInteractionHandler();
}

/// <summary>
/// Initializes a new instance of the <see cref="ContextMenu"/> class.
/// </summary>
/// <param name="interactionHandler">The menu iteraction handler.</param>
public ContextMenu(IMenuInteractionHandler interactionHandler)
{
Contract.Requires<ArgumentNullException>(interactionHandler != null);

_interaction = interactionHandler;
}

/// <summary>
/// Initializes static members of the <see cref="ContextMenu"/> class.
/// </summary>
static ContextMenu()
{
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);

MenuItem.ClickEvent.AddClassHandler<ContextMenu>(x => x.OnContextMenuClick, handledEventsToo: true);
}

/// <summary>
/// Gets a value indicating whether the popup is open
/// </summary>
public bool IsOpen => _isOpen;

/// <inheritdoc/>
IMenuInteractionHandler IMenu.InteractionHandler => _interaction;

/// <inheritdoc/>
IMenuItem IMenuElement.SelectedItem
{
get
{
var index = SelectedIndex;
return (index != -1) ?
(IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) :
null;
}
set
{
SelectedIndex = ItemContainerGenerator.IndexFromContainer(value);
}
}

/// <inheritdoc/>
IEnumerable<IMenuItem> IMenuElement.SubItems
{
get
{
return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl)
.OfType<IMenuItem>();
}
}

/// <summary>
/// Occurs when the value of the
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
Expand All @@ -50,7 +99,6 @@ static ContextMenu()
/// </summary>
public event CancelEventHandler ContextMenuClosing;


/// <summary>
/// Called when the <see cref="Control.ContextMenu"/> property changes on a control.
/// </summary>
Expand All @@ -71,62 +119,53 @@ private static void ContextMenuChanged(AvaloniaPropertyChangedEventArgs e)
}

/// <summary>
/// Called when a submenu is clicked somewhere in the menu.
/// Opens the menu.
/// </summary>
/// <param name="e">The event args.</param>
private void OnContextMenuClick(RoutedEventArgs e)
{
Hide();
FocusManager.Instance.Focus(null);
e.Handled = true;
}
public void Open() => Open(null);

/// <summary>
/// Closes the menu.
/// Opens a context menu on the specified control.
/// </summary>
public void Hide()
/// <param name="control">The control.</param>
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);
}

/// <summary>
/// Shows a context menu for the specified control.
/// Closes the menu.
/// </summary>
/// <param name="control">The control.</param>
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;

Expand All @@ -152,7 +191,7 @@ private static void ControlPointerReleased(object sender, PointerReleasedEventAr
if (contextMenu.CancelClosing())
return;

control.ContextMenu.Hide();
control.ContextMenu.Close();
e.Handled = true;
}

Expand All @@ -161,7 +200,7 @@ private static void ControlPointerReleased(object sender, PointerReleasedEventAr
if (contextMenu.CancelOpening())
return;

contextMenu.Show(control);
contextMenu.Open(control);
e.Handled = true;
}
}
Expand All @@ -179,5 +218,10 @@ private bool CancelOpening()
ContextMenuOpening?.Invoke(this, eventArgs);
return eventArgs.Cancel;
}

bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap)
{
throw new NotImplementedException();
}
}
}
21 changes: 21 additions & 0 deletions src/Avalonia.Controls/IMenu.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using Avalonia.Controls.Platform;

namespace Avalonia.Controls
{
/// <summary>
/// Represents a <see cref="Menu"/> or <see cref="ContextMenu"/>.
/// </summary>
public interface IMenu : IMenuElement
{
/// <summary>
/// Gets the menu interaction handler.
/// </summary>
IMenuInteractionHandler InteractionHandler { get; }

/// <summary>
/// Gets a value indicating whether the menu is open.
/// </summary>
bool IsOpen { get; }
}
}
40 changes: 40 additions & 0 deletions src/Avalonia.Controls/IMenuElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using Avalonia.Input;

namespace Avalonia.Controls
{
/// <summary>
/// Represents an <see cref="IMenu"/> or <see cref="IMenuItem"/>.
/// </summary>
public interface IMenuElement : IControl
{
/// <summary>
/// Gets or sets the currently selected submenu item.
/// </summary>
IMenuItem SelectedItem { get; set; }

/// <summary>
/// Gets the submenu items.
/// </summary>
IEnumerable<IMenuItem> SubItems { get; }

/// <summary>
/// Opens the menu or menu item.
/// </summary>
void Open();

/// <summary>
/// Closes the menu or menu item.
/// </summary>
void Close();

/// <summary>
/// Moves the submenu selection in the specified direction.
/// </summary>
/// <param name="direction">The direction.</param>
/// <param name="wrap">Whether to wrap after the first or last item.</param>
/// <returns>True if the selection was moved; otherwise false.</returns>
bool MoveSelection(NavigationDirection direction, bool wrap);
}
}
41 changes: 41 additions & 0 deletions src/Avalonia.Controls/IMenuItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;

namespace Avalonia.Controls
{
/// <summary>
/// Represents a <see cref="MenuItem"/>.
/// </summary>
public interface IMenuItem : IMenuElement
{
/// <summary>
/// Gets or sets a value that indicates whether the item has a submenu.
/// </summary>
bool HasSubMenu { get; }

/// <summary>
/// Gets a value indicating whether the mouse is currently over the menu item's submenu.
/// </summary>
bool IsPointerOverSubMenu { get; }

/// <summary>
/// Gets or sets a value that indicates whether the submenu of the <see cref="MenuItem"/> is
/// open.
/// </summary>
bool IsSubMenuOpen { get; set; }

/// <summary>
/// Gets a value that indicates whether the <see cref="MenuItem"/> is a top-level main menu item.
/// </summary>
bool IsTopLevel { get; }

/// <summary>
/// Gets the parent <see cref="IMenuElement"/>.
/// </summary>
new IMenuElement Parent { get; }

/// <summary>
/// Raises a click event on the menu item.
/// </summary>
void RaiseClick();
}
}
Loading