Browse Source

Merge pull request #3751 from AvaloniaUI/fixes/497-shared-contextmenu

Allow ContextMenus to be shared
pull/3937/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
8d8fa16103
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 39
      src/Avalonia.Controls/ContextMenu.cs
  2. 87
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

39
src/Avalonia.Controls/ContextMenu.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Generators;
@ -9,18 +10,19 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls
{
/// <summary>
/// A control context menu.
/// </summary>
public class ContextMenu : MenuBase
public class ContextMenu : MenuBase, ISetterValue
{
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup;
private Control _attachedControl;
private List<Control> _attachedControls;
private IInputElement _previousFocus;
/// <summary>
@ -74,13 +76,14 @@ namespace Avalonia.Controls
if (e.OldValue is ContextMenu oldMenu)
{
control.PointerReleased -= ControlPointerReleased;
oldMenu._attachedControl = null;
oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent)oldMenu._popup)?.SetParent(null);
}
if (e.NewValue is ContextMenu newMenu)
{
newMenu._attachedControl = control;
newMenu._attachedControls ??= new List<Control>();
newMenu._attachedControls.Add(control);
control.PointerReleased += ControlPointerReleased;
}
}
@ -96,18 +99,22 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param>
public void Open(Control control)
{
if (control is null && _attachedControl is null)
if (control is null && (_attachedControls is null || _attachedControls.Count == 0))
{
throw new ArgumentNullException(nameof(control));
}
if (control is object && _attachedControl is object && control != _attachedControl)
if (control is object &&
_attachedControls is object &&
!_attachedControls.Contains(control))
{
throw new ArgumentException(
"Cannot show ContentMenu on a different control to the one it is attached to.",
nameof(control));
}
control ??= _attachedControls[0];
if (IsOpen)
{
return;
@ -126,7 +133,12 @@ namespace Avalonia.Controls
_popup.Closed += PopupClosed;
}
((ISetLogicalParent)_popup).SetParent(control);
if (_popup.Parent != control)
{
((ISetLogicalParent)_popup).SetParent(null);
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.Child = this;
_popup.IsOpen = true;
@ -155,6 +167,17 @@ namespace Avalonia.Controls
}
}
void ISetterValue.Initialize(ISetter setter)
{
// ContextMenu can be assigned to the ContextMenu property in a setter. This overrides
// the behavior defined in Control which requires controls to be wrapped in a <template>.
if (!(setter is Setter s && s.Property == ContextMenuProperty))
{
throw new InvalidOperationException(
"Cannot use a control as a Setter value. Wrap the control in a <Template>.");
}
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
@ -179,7 +202,7 @@ namespace Avalonia.Controls
SelectedIndex = -1;
IsOpen = false;
if (_attachedControl is null)
if (_attachedControls is null || _attachedControls.Count == 0)
{
((ISetLogicalParent)_popup).SetParent(null);
}

87
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -1,7 +1,10 @@
using System;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Castle.DynamicProxy.Generators;
using Moq;
using Xunit;
@ -168,6 +171,90 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Context_Menu_In_Resources_Can_Be_Shared()
{
using (Application())
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Resources>
<ContextMenu x:Key='contextMenu'>
<MenuItem>Foo</MenuItem>
</ContextMenu>
</Window.Resources>
<StackPanel>
<TextBlock Name='target1' ContextMenu='{StaticResource contextMenu}'/>
<TextBlock Name='target2' ContextMenu='{StaticResource contextMenu}'/>
</StackPanel>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var target1 = window.Find<TextBlock>("target1");
var target2 = window.Find<TextBlock>("target2");
var mouse = new MouseTestHelper();
Assert.NotNull(target1.ContextMenu);
Assert.NotNull(target2.ContextMenu);
Assert.Same(target1.ContextMenu, target2.ContextMenu);
window.Show();
var menu = target1.ContextMenu;
mouse.Click(target1, MouseButton.Right);
Assert.True(menu.IsOpen);
mouse.Click(target2, MouseButton.Right);
Assert.True(menu.IsOpen);
}
}
[Fact]
public void Context_Menu_Can_Be_Set_In_Style()
{
using (Application())
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<Style Selector='TextBlock'>
<Setter Property='ContextMenu'>
<ContextMenu>
<MenuItem>Foo</MenuItem>
</ContextMenu>
</Setter>
</Style>
</Window.Styles>
<StackPanel>
<TextBlock Name='target1'/>
<TextBlock Name='target2'/>
</StackPanel>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var target1 = window.Find<TextBlock>("target1");
var target2 = window.Find<TextBlock>("target2");
var mouse = new MouseTestHelper();
Assert.NotNull(target1.ContextMenu);
Assert.NotNull(target2.ContextMenu);
Assert.Same(target1.ContextMenu, target2.ContextMenu);
window.Show();
var menu = target1.ContextMenu;
mouse.Click(target1, MouseButton.Right);
Assert.True(menu.IsOpen);
mouse.Click(target2, MouseButton.Right);
Assert.True(menu.IsOpen);
}
}
[Fact(Skip = "The only reason this test was 'passing' before was that the author forgot to call Window.ApplyTemplate()")]
public void Cancelling_Closing_Leaves_ContextMenuOpen()
{

Loading…
Cancel
Save