Browse Source

Added MenuBase.

And use it as base class for both `Menu` and `ContextMenu`.
pull/2332/head
Steven Kirk 7 years ago
parent
commit
fbfd5d80d1
  1. 85
      src/Avalonia.Controls/ContextMenu.cs
  2. 151
      src/Avalonia.Controls/Menu.cs
  3. 193
      src/Avalonia.Controls/MenuBase.cs

85
src/Avalonia.Controls/ContextMenu.cs

@ -1,34 +1,29 @@
using System; using System;
using System.Reactive.Linq;
using System.Linq;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using System.Collections.Generic; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
public class ContextMenu : SelectingItemsControl, IMenu /// <summary>
/// A control context menu.
/// </summary>
public class ContextMenu : MenuBase
{ {
private readonly IMenuInteractionHandler _interaction; private static readonly ITemplate<IPanel> DefaultPanel =
private bool _isOpen; new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup; private Popup _popup;
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
public static readonly DirectProperty<ContextMenu, bool> IsOpenProperty =
AvaloniaProperty.RegisterDirect<ContextMenu, bool>(nameof(IsOpen), o => o.IsOpen);
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ContextMenu"/> class. /// Initializes a new instance of the <see cref="ContextMenu"/> class.
/// </summary> /// </summary>
public ContextMenu() public ContextMenu()
{ {
_interaction = AvaloniaLocator.Current.GetService<IMenuInteractionHandler>() ??
new DefaultMenuInteractionHandler();
} }
/// <summary> /// <summary>
@ -36,10 +31,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
/// <param name="interactionHandler">The menu interaction handler.</param> /// <param name="interactionHandler">The menu interaction handler.</param>
public ContextMenu(IMenuInteractionHandler interactionHandler) public ContextMenu(IMenuInteractionHandler interactionHandler)
: base(interactionHandler)
{ {
Contract.Requires<ArgumentNullException>(interactionHandler != null);
_interaction = interactionHandler;
} }
/// <summary> /// <summary>
@ -47,44 +40,10 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static ContextMenu() static ContextMenu()
{ {
ItemsPanelProperty.OverrideDefaultValue(typeof(ContextMenu), DefaultPanel);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
} }
/// <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> /// <summary>
/// Occurs when the value of the /// Occurs when the value of the
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" /> /// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
@ -121,7 +80,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Opens the menu. /// Opens the menu.
/// </summary> /// </summary>
public void Open() => Open(null); public override void Open() => Open(null);
/// <summary> /// <summary>
/// Opens a context menu on the specified control. /// Opens a context menu on the specified control.
@ -140,20 +99,18 @@ namespace Avalonia.Controls
}; };
_popup.Closed += PopupClosed; _popup.Closed += PopupClosed;
_interaction.Attach(this);
} }
((ISetLogicalParent)_popup).SetParent(control); ((ISetLogicalParent)_popup).SetParent(control);
_popup.Child = this; _popup.Child = this;
_popup.IsOpen = true; _popup.IsOpen = true;
IsOpen = true;
SetAndRaise(IsOpenProperty, ref _isOpen, true);
} }
/// <summary> /// <summary>
/// Closes the menu. /// Closes the menu.
/// </summary> /// </summary>
public void Close() public override void Close()
{ {
if (_popup != null && _popup.IsVisible) if (_popup != null && _popup.IsVisible)
{ {
@ -161,8 +118,7 @@ namespace Avalonia.Controls
} }
SelectedIndex = -1; SelectedIndex = -1;
IsOpen = false;
SetAndRaise(IsOpenProperty, ref _isOpen, false);
} }
private void PopupClosed(object sender, EventArgs e) private void PopupClosed(object sender, EventArgs e)
@ -176,7 +132,7 @@ namespace Avalonia.Controls
i.IsSubMenuOpen = false; i.IsSubMenuOpen = false;
} }
contextMenu._isOpen = false; contextMenu.IsOpen = false;
contextMenu.SelectedIndex = -1; contextMenu.SelectedIndex = -1;
} }
} }
@ -186,7 +142,7 @@ namespace Avalonia.Controls
var control = (Control)sender; var control = (Control)sender;
var contextMenu = control.ContextMenu; var contextMenu = control.ContextMenu;
if (control.ContextMenu._isOpen) if (control.ContextMenu.IsOpen)
{ {
if (contextMenu.CancelClosing()) if (contextMenu.CancelClosing())
return; return;
@ -218,10 +174,5 @@ namespace Avalonia.Controls
ContextMenuOpening?.Invoke(this, eventArgs); ContextMenuOpening?.Invoke(this, eventArgs);
return eventArgs.Cancel; return eventArgs.Cancel;
} }
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap)
{
throw new NotImplementedException();
}
} }
} }

151
src/Avalonia.Controls/Menu.cs

@ -1,56 +1,26 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // 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.Controls.Generators;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.LogicalTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// A top-level menu control. /// A top-level menu control.
/// </summary> /// </summary>
public class Menu : SelectingItemsControl, IFocusScope, IMainMenu, IMenu public class Menu : MenuBase, IFocusScope, IMainMenu
{ {
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
public static readonly DirectProperty<Menu, bool> IsOpenProperty =
AvaloniaProperty.RegisterDirect<Menu, bool>(
nameof(IsOpen),
o => o.IsOpen);
/// <summary>
/// Defines the <see cref="MenuOpened"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> MenuOpenedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuOpened), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="MenuClosed"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> MenuClosedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuClosed), RoutingStrategies.Bubble);
private static readonly ITemplate<IPanel> DefaultPanel = private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Horizontal }); new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Horizontal });
private readonly IMenuInteractionHandler _interaction;
private bool _isOpen;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Menu"/> class. /// Initializes a new instance of the <see cref="Menu"/> class.
/// </summary> /// </summary>
public Menu() public Menu()
{ {
_interaction = AvaloniaLocator.Current.GetService<IMenuInteractionHandler>() ??
new DefaultMenuInteractionHandler();
} }
/// <summary> /// <summary>
@ -58,82 +28,17 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
/// <param name="interactionHandler">The menu interaction handler.</param> /// <param name="interactionHandler">The menu interaction handler.</param>
public Menu(IMenuInteractionHandler interactionHandler) public Menu(IMenuInteractionHandler interactionHandler)
: base(interactionHandler)
{ {
Contract.Requires<ArgumentNullException>(interactionHandler != null);
_interaction = interactionHandler;
} }
/// <summary>
/// Initializes static members of the <see cref="Menu"/> class.
/// </summary>
static Menu() static Menu()
{ {
ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel);
MenuItem.SubmenuOpenedEvent.AddClassHandler<Menu>(x => x.OnSubmenuOpened);
}
/// <summary>
/// Gets a value indicating whether the menu is open.
/// </summary>
public bool IsOpen
{
get { return _isOpen; }
private set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
}
/// <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/> /// <inheritdoc/>
IEnumerable<IMenuItem> IMenuElement.SubItems public override void Close()
{
get
{
return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl)
.OfType<IMenuItem>();
}
}
/// <summary>
/// Occurs when a <see cref="Menu"/> is opened.
/// </summary>
public event EventHandler<RoutedEventArgs> MenuOpened
{
add { AddHandler(MenuOpenedEvent, value); }
remove { RemoveHandler(MenuOpenedEvent, value); }
}
/// <summary>
/// Occurs when a <see cref="Menu"/> is closed.
/// </summary>
public event EventHandler<RoutedEventArgs> MenuClosed
{
add { AddHandler(MenuClosedEvent, value); }
remove { RemoveHandler(MenuClosedEvent, value); }
}
/// <summary>
/// Closes the menu.
/// </summary>
public void Close()
{ {
if (IsOpen) if (IsOpen)
{ {
@ -153,10 +58,8 @@ namespace Avalonia.Controls
} }
} }
/// <summary> /// <inheritdoc/>
/// Opens the menu in response to the Alt/F10 key. public override void Open()
/// </summary>
public void Open()
{ {
if (!IsOpen) if (!IsOpen)
{ {
@ -170,15 +73,6 @@ namespace Avalonia.Controls
} }
} }
/// <inheritdoc/>
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<MenuItem>(this, MenuItem.HeaderProperty, null);
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
@ -190,41 +84,6 @@ namespace Avalonia.Controls
{ {
inputRoot.AccessKeyHandler.MainMenu = this; inputRoot.AccessKeyHandler.MainMenu = this;
} }
_interaction.Attach(this);
}
/// <inheritdoc/>
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_interaction.Detach(this);
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
// Don't handle here: let the interaction handler handle it.
}
/// <summary>
/// Called when a submenu opens somewhere in the menu.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnSubmenuOpened(RoutedEventArgs e)
{
if (e.Source is MenuItem menuItem && menuItem.Parent == this)
{
foreach (var child in this.GetLogicalChildren().OfType<MenuItem>())
{
if (child != menuItem && child.IsSubMenuOpen)
{
child.IsSubMenuOpen = false;
}
}
}
IsOpen = true;
} }
} }
} }

193
src/Avalonia.Controls/MenuBase.cs

@ -0,0 +1,193 @@
// 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.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
namespace Avalonia.Controls
{
/// <summary>
/// Base class for menu controls.
/// </summary>
public abstract class MenuBase : SelectingItemsControl, IMenu
{
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
public static readonly DirectProperty<Menu, bool> IsOpenProperty =
AvaloniaProperty.RegisterDirect<Menu, bool>(
nameof(IsOpen),
o => o.IsOpen);
/// <summary>
/// Defines the <see cref="MenuOpened"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> MenuOpenedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuOpened), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="MenuClosed"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> MenuClosedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuClosed), RoutingStrategies.Bubble);
private bool _isOpen;
/// <summary>
/// Initializes a new instance of the <see cref="MenuBase"/> class.
/// </summary>
public MenuBase()
{
InteractionHandler = AvaloniaLocator.Current.GetService<IMenuInteractionHandler>() ??
new DefaultMenuInteractionHandler();
}
/// <summary>
/// Initializes a new instance of the <see cref="MenuBase"/> class.
/// </summary>
/// <param name="interactionHandler">The menu interaction handler.</param>
public MenuBase(IMenuInteractionHandler interactionHandler)
{
Contract.Requires<ArgumentNullException>(interactionHandler != null);
InteractionHandler = interactionHandler;
}
/// <summary>
/// Initializes static members of the <see cref="MenuBase"/> class.
/// </summary>
static MenuBase()
{
MenuItem.SubmenuOpenedEvent.AddClassHandler<MenuBase>(x => x.OnSubmenuOpened);
}
/// <summary>
/// Gets a value indicating whether the menu is open.
/// </summary>
public bool IsOpen
{
get { return _isOpen; }
protected set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
}
/// <inheritdoc/>
IMenuInteractionHandler IMenu.InteractionHandler => InteractionHandler;
/// <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>
/// Gets the interaction handler for the menu.
/// </summary>
protected IMenuInteractionHandler InteractionHandler { get; }
/// <summary>
/// Occurs when a <see cref="Menu"/> is opened.
/// </summary>
public event EventHandler<RoutedEventArgs> MenuOpened
{
add { AddHandler(MenuOpenedEvent, value); }
remove { RemoveHandler(MenuOpenedEvent, value); }
}
/// <summary>
/// Occurs when a <see cref="Menu"/> is closed.
/// </summary>
public event EventHandler<RoutedEventArgs> MenuClosed
{
add { AddHandler(MenuClosedEvent, value); }
remove { RemoveHandler(MenuClosedEvent, value); }
}
/// <summary>
/// Closes the menu.
/// </summary>
public abstract void Close();
/// <summary>
/// Opens the menu.
/// </summary>
public abstract void Open();
/// <inheritdoc/>
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<MenuItem>(this, MenuItem.HeaderProperty, null);
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
// Don't handle here: let the interaction handler handle it.
}
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
InteractionHandler.Attach(this);
}
/// <inheritdoc/>
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
InteractionHandler.Detach(this);
}
/// <summary>
/// Called when a submenu opens somewhere in the menu.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnSubmenuOpened(RoutedEventArgs e)
{
if (e.Source is MenuItem menuItem && menuItem.Parent == this)
{
foreach (var child in this.GetLogicalChildren().OfType<MenuItem>())
{
if (child != menuItem && child.IsSubMenuOpen)
{
child.IsSubMenuOpen = false;
}
}
}
IsOpen = true;
}
}
}
Loading…
Cancel
Save