Browse Source

Merge pull request #405 from VitalElement/master

Implementation of Context Menus
Steven Kirk 9 years ago
parent
commit
3277d7aa1a

+ 15 - 1
samples/XamlTestApplicationPcl/Views/MainWindow.paml

@@ -38,7 +38,21 @@
           </TabItem>
           <TabItem Header="Text">
               <StackPanel Margin="10" Gap="4">
-                  <TextBlock Text="TextBlock" FontWeight="Medium" FontSize="20" Foreground="#212121" />
+                  <TextBlock Text="TextBlock with ContextMenu" FontWeight="Medium" FontSize="20" Foreground="#212121">
+                    <ContextMenu.ContextMenu>
+                      <ContextMenu>
+                        <MenuItem Header="Testing 1" />
+                        <MenuItem Header="Testing 2" />
+                        <MenuItem Header="Testing 3" />
+                        <MenuItem Header="Testing 4">
+                          <MenuItem Header="SubItem 1" />
+                          <MenuItem Header="SubItem 2" />
+                          <MenuItem Header="SubItem 3" />
+                          <MenuItem Header="SubItem 4" />
+                        </MenuItem>
+                      </ContextMenu>
+                    </ContextMenu.ContextMenu>
+                  </TextBlock>
                   <TextBlock Text="A control for displaying text."
                               FontSize="13"
                               Foreground="#727272"

+ 149 - 0
src/Perspex.Controls/ContextMenu.cs

@@ -0,0 +1,149 @@
+namespace Perspex.Controls
+{
+    using Input;
+    using Interactivity;
+    using LogicalTree;
+    using Primitives;
+    using System;
+    using System.Reactive.Linq;
+
+    public class ContextMenu : SelectingItemsControl
+    {
+        private bool _isOpen;
+
+        /// <summary>
+        /// The popup window used to display the active context menu.
+        /// </summary>
+        private static Popup _popup;
+
+        /// <summary>
+        /// Initializes static members of the <see cref="ContextMenu"/> class.
+        /// </summary>
+        static ContextMenu()
+        {
+            ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
+
+            MenuItem.ClickEvent.AddClassHandler<ContextMenu>(x => x.OnContextMenuClick);            
+        }
+
+        /// <summary>
+        /// called when the <see cref="ContextMenuProperty"/> property changes on a control.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void ContextMenuChanged(PerspexPropertyChangedEventArgs e)
+        {
+            var control = (Control)e.Sender;
+
+            if (e.OldValue != null)
+            {
+                control.PointerReleased -= ControlPointerReleased;
+            }
+
+            if (e.NewValue != null)
+            {
+                control.PointerReleased += ControlPointerReleased;
+            }
+        }
+
+        /// <summary>
+        /// Called when a submenu is clicked somewhere in the menu.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private void OnContextMenuClick(RoutedEventArgs e)
+        {
+            Hide();
+            FocusManager.Instance.Focus(null);
+            e.Handled = true;
+        }
+
+        /// <summary>
+        /// Closes the menu.
+        /// </summary>
+        public void Hide()
+        {
+            
+
+            if (_popup != null && _popup.IsVisible)
+            {
+                _popup.Close();
+            }
+
+            SelectedIndex = -1;
+
+            _isOpen = false;
+        }
+
+        /// <summary>
+        /// Shows a context menu for the specified control.
+        /// </summary>
+        /// <param name="control">The control.</param>
+        private static void Show(Control control)
+        {
+            if (control != null)
+            {
+                if(_popup == null)
+                {
+                    _popup = new Popup()
+                    {
+                        PlacementTarget = control,
+                        StaysOpen = false                                         
+                    };
+
+                    _popup.Closed += PopupClosed;
+                }
+                 
+                ((ISetLogicalParent)_popup).SetParent(control);
+                _popup.Child = control.ContextMenu;
+
+                _popup.Open();
+
+                control.ContextMenu._isOpen = true;
+            }
+        }
+
+        private static void PopupClosed(object sender, EventArgs e)
+        {
+            var contextMenu = (sender as Popup)?.Child as ContextMenu;
+
+            if (contextMenu != null)
+            {
+                foreach (MenuItem i in contextMenu.GetLogicalChildren())
+                {
+                    i.IsSubMenuOpen = false;
+                }
+
+                contextMenu._isOpen = false;
+            }
+        }
+
+        private void PopupOpened(object sender, EventArgs e)
+        {
+            var selectedIndex = SelectedIndex;
+
+            if (selectedIndex != -1)
+            {
+                var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
+                container?.Focus();
+            }
+        }
+
+        private static void ControlPointerReleased(object sender, PointerReleasedEventArgs e)
+        {
+            var control = (Control)sender;
+
+            if (e.MouseButton == MouseButton.Right)
+            {
+                if (control.ContextMenu._isOpen)
+                {
+                    control.ContextMenu.Hide();
+                }
+
+                Show(control);
+            }
+            else
+            {
+                control.ContextMenu.Hide();
+            }
+        }
+    }
+}

+ 16 - 0
src/Perspex.Controls/Control.cs

@@ -65,6 +65,13 @@ namespace Perspex.Controls
         public static readonly StyledProperty<ITemplatedControl> TemplatedParentProperty =
             PerspexProperty.Register<Control, ITemplatedControl>(nameof(TemplatedParent), inherits: true);
 
+        /// <summary>
+        /// Defines the <see cref="ContextMenu"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ContextMenu> ContextMenuProperty =
+            PerspexProperty.Register<Control, ContextMenu>(nameof(ContextMenu));        
+
+
         /// <summary>
         /// Event raised when an element wishes to be scrolled into view.
         /// </summary>
@@ -204,6 +211,15 @@ namespace Perspex.Controls
         /// </summary>
         public IControl Parent => _parent;
 
+        /// <summary>
+        /// Gets or sets a context menu to the control.
+        /// </summary>
+        public ContextMenu ContextMenu
+        {
+            get { return GetValue(ContextMenuProperty); }
+            set { SetValue(ContextMenuProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets a user-defined object attached to the control.
         /// </summary>

+ 1 - 0
src/Perspex.Controls/Perspex.Controls.csproj

@@ -43,6 +43,7 @@
       <Link>Properties\SharedAssemblyInfo.cs</Link>
     </Compile>
     <Compile Include="Classes.cs" />
+    <Compile Include="ContextMenu.cs" />
     <Compile Include="DockPanel.cs" />
     <Compile Include="Expander.cs" />
     <Compile Include="Generators\ItemContainer.cs" />

+ 16 - 6
src/Perspex.Controls/Primitives/Popup.cs

@@ -7,6 +7,8 @@ using Perspex.Interactivity;
 using Perspex.Metadata;
 using Perspex.Rendering;
 using Perspex.VisualTree;
+using Perspex.LogicalTree;
+using System.Linq;
 
 namespace Perspex.Controls.Primitives
 {
@@ -170,6 +172,11 @@ namespace Perspex.Controls.Primitives
 
             _popupRoot.Position = GetPosition();
 
+            if (_topLevel == null && PlacementTarget != null)
+            {
+                _topLevel = PlacementTarget.GetSelfAndLogicalAncestors().First(x => x is TopLevel) as TopLevel;
+            }
+
             if (_topLevel != null)
             {
                 _topLevel.Deactivated += TopLevelDeactivated;
@@ -190,8 +197,12 @@ namespace Perspex.Controls.Primitives
         {
             if (_popupRoot != null)
             {
-                _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
-                _topLevel.Deactivated -= TopLevelDeactivated;
+                if (_topLevel != null)
+                {
+                    _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
+                    _topLevel.Deactivated -= TopLevelDeactivated;
+                }
+
                 _popupRoot.Hide();
             }
 
@@ -210,16 +221,15 @@ namespace Perspex.Controls.Primitives
         }
 
         /// <inheritdoc/>
-        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
-            base.OnAttachedToVisualTree(e);
+            base.OnAttachedToLogicalTree(e);
             _topLevel = e.Root as TopLevel;
         }
 
         /// <inheritdoc/>
-        protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+        protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
-            base.OnDetachedFromVisualTree(e);
             _topLevel = null;
         }
 

+ 20 - 0
src/Perspex.Themes.Default/ContextMenu.paml

@@ -0,0 +1,20 @@
+<Style xmlns="https://github.com/perspex" Selector="ContextMenu">
+    <Setter Property="BorderBrush" Value="Gray"/>
+    <Setter Property="BorderThickness" Value="1"/>
+    <Setter Property="Padding" Value="4,2"/>
+    <Setter Property="FontSize" Value="12" />
+    <Setter Property="FontWeight" Value="Normal" />
+    <Setter Property="Template">
+    <ControlTemplate>
+      <Border Background="{TemplateBinding Background}"
+              BorderBrush="{TemplateBinding BorderBrush}"
+              BorderThickness="{TemplateBinding BorderThickness}"
+              Padding="{TemplateBinding Padding}">
+        <ItemsPresenter Name="PART_ItemsPresenter"
+                        Items="{TemplateBinding Items}"
+                        ItemsPanel="{TemplateBinding ItemsPanel}"
+                        KeyboardNavigation.TabNavigation="Continue"/>
+      </Border>
+    </ControlTemplate>
+  </Setter>
+</Style>

+ 1 - 0
src/Perspex.Themes.Default/DefaultTheme.paml

@@ -11,6 +11,7 @@
   <StyleInclude Source="resm:Perspex.Themes.Default.ListBox.paml?assembly=Perspex.Themes.Default"/>
   <StyleInclude Source="resm:Perspex.Themes.Default.ListBoxItem.paml?assembly=Perspex.Themes.Default"/>
   <StyleInclude Source="resm:Perspex.Themes.Default.Menu.paml?assembly=Perspex.Themes.Default"/>
+  <StyleInclude Source="resm:Perspex.Themes.Default.ContextMenu.paml?assembly=Perspex.Themes.Default"/>
   <StyleInclude Source="resm:Perspex.Themes.Default.MenuItem.paml?assembly=Perspex.Themes.Default"/>
   <StyleInclude Source="resm:Perspex.Themes.Default.PopupRoot.paml?assembly=Perspex.Themes.Default"/>
   <StyleInclude Source="resm:Perspex.Themes.Default.ProgressBar.paml?assembly=Perspex.Themes.Default"/>

+ 3 - 0
src/Perspex.Themes.Default/Perspex.Themes.Default.csproj

@@ -147,6 +147,9 @@
     <EmbeddedResource Include="DropDownItem.paml">
       <SubType>Designer</SubType>
     </EmbeddedResource>
+    <EmbeddedResource Include="ContextMenu.paml">
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
     <None Include="packages.config" />
     <EmbeddedResource Include="PopupRoot.paml">
       <SubType>Designer</SubType>