Browse Source

Added MenuBase.

And use it as base class for both `Menu` and `ContextMenu`.
Steven Kirk 6 years ago
parent
commit
fbfd5d80d1
3 changed files with 216 additions and 213 deletions
  1. 18 67
      src/Avalonia.Controls/ContextMenu.cs
  2. 5 146
      src/Avalonia.Controls/Menu.cs
  3. 193 0
      src/Avalonia.Controls/MenuBase.cs

+ 18 - 67
src/Avalonia.Controls/ContextMenu.cs

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

+ 5 - 146
src/Avalonia.Controls/Menu.cs

@@ -1,56 +1,26 @@
 // 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>
     /// A top-level menu control.
     /// </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 =
             new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Horizontal });
-        private readonly IMenuInteractionHandler _interaction;
-        private bool _isOpen;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="Menu"/> class.
         /// </summary>
         public Menu()
         {
-            _interaction = AvaloniaLocator.Current.GetService<IMenuInteractionHandler>() ?? 
-                new DefaultMenuInteractionHandler();
         }
 
         /// <summary>
@@ -58,82 +28,17 @@ namespace Avalonia.Controls
         /// </summary>
         /// <param name="interactionHandler">The menu interaction handler.</param>
         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()
         {
             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/>
-        IEnumerable<IMenuItem> IMenuElement.SubItems
-        {
-            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()
+        public override void Close()
         {
             if (IsOpen)
             {
@@ -153,10 +58,8 @@ namespace Avalonia.Controls
             }
         }
 
-        /// <summary>
-        /// Opens the menu in response to the Alt/F10 key.
-        /// </summary>
-        public void Open()
+        /// <inheritdoc/>
+        public override void Open()
         {
             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/>
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {
@@ -190,41 +84,6 @@ namespace Avalonia.Controls
             {
                 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 - 0
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;
+        }
+    }
+}