Browse Source

Merge branch 'master' into issues/3692

pull/3693/head
Steven Kirk 6 years ago
committed by GitHub
parent
commit
d418d71d82
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 59
      src/Avalonia.Controls/ContextMenu.cs
  2. 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  3. 2
      src/Avalonia.Controls/WindowBase.cs
  4. 2
      src/Avalonia.Input/FocusManager.cs
  5. 107
      src/Avalonia.X11/X11ImmediateRendererProxy.cs
  6. 1
      src/Avalonia.X11/X11Platform.cs
  7. 29
      src/Avalonia.X11/X11Window.cs
  8. 17
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  9. 78
      tests/Avalonia.LeakTests/ControlTests.cs
  10. 4
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs

59
src/Avalonia.Controls/ContextMenu.cs

@ -20,6 +20,8 @@ namespace Avalonia.Controls
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup;
private Control _attachedControl;
private IInputElement _previousFocus;
/// <summary>
/// Initializes a new instance of the <see cref="ContextMenu"/> class.
@ -69,13 +71,16 @@ namespace Avalonia.Controls
{
var control = (Control)e.Sender;
if (e.OldValue != null)
if (e.OldValue is ContextMenu oldMenu)
{
control.PointerReleased -= ControlPointerReleased;
oldMenu._attachedControl = null;
((ISetLogicalParent)oldMenu._popup)?.SetParent(null);
}
if (e.NewValue != null)
if (e.NewValue is ContextMenu newMenu)
{
newMenu._attachedControl = control;
control.PointerReleased += ControlPointerReleased;
}
}
@ -91,8 +96,18 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param>
public void Open(Control control)
{
if (control == null)
if (control is null && _attachedControl is null)
{
throw new ArgumentNullException(nameof(control));
}
if (control is object && _attachedControl is object && control != _attachedControl)
{
throw new ArgumentException(
"Cannot show ContentMenu on a different control to the one it is attached to.",
nameof(control));
}
if (IsOpen)
{
return;
@ -145,36 +160,38 @@ namespace Avalonia.Controls
return new MenuItemContainerGenerator(this);
}
private void CloseCore()
{
SelectedIndex = -1;
IsOpen = false;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
}
private void PopupOpened(object sender, EventArgs e)
{
_previousFocus = FocusManager.Instance?.Current;
Focus();
}
private void PopupClosed(object sender, EventArgs e)
{
var contextMenu = (sender as Popup)?.Child as ContextMenu;
if (contextMenu != null)
foreach (var i in LogicalChildren)
{
foreach (var i in contextMenu.GetLogicalChildren().OfType<MenuItem>())
if (i is MenuItem menuItem)
{
i.IsSubMenuOpen = false;
menuItem.IsSubMenuOpen = false;
}
}
contextMenu.CloseCore();
SelectedIndex = -1;
IsOpen = false;
if (_attachedControl is null)
{
((ISetLogicalParent)_popup).SetParent(null);
}
// HACK: Reset the focus when the popup is closed. We need to fix this so it's automatic.
FocusManager.Instance?.Focus(_previousFocus);
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
}
private static void ControlPointerReleased(object sender, PointerReleasedEventArgs e)

2
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -96,7 +96,7 @@ namespace Avalonia.Controls.Platform
root.Deactivated -= WindowDeactivated;
}
_inputManagerSubscription!.Dispose();
_inputManagerSubscription?.Dispose();
Menu = null;
_root = null;

2
src/Avalonia.Controls/WindowBase.cs

@ -255,7 +255,7 @@ namespace Avalonia.Controls
if (scope != null)
{
FocusManager.Instance.SetFocusScope(scope);
FocusManager.Instance?.SetFocusScope(scope);
}
IsActive = true;

2
src/Avalonia.Input/FocusManager.cs

@ -168,7 +168,7 @@ namespace Avalonia.Input
{
var scope = control as IFocusScope;
if (scope != null)
if (scope != null && control.VisualRoot?.IsVisible == true)
{
yield return scope;
}

107
src/Avalonia.X11/X11ImmediateRendererProxy.cs

@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.X11
{
public class X11ImmediateRendererProxy : IRenderer, IRenderLoopTask
{
private readonly IRenderLoop _loop;
private ImmediateRenderer _renderer;
private bool _invalidated;
private object _lock = new object();
public X11ImmediateRendererProxy(IVisual root, IRenderLoop loop)
{
_loop = loop;
_renderer = new ImmediateRenderer(root);
}
public void Dispose()
{
_renderer.Dispose();
}
public bool DrawFps
{
get => _renderer.DrawFps;
set => _renderer.DrawFps = value;
}
public bool DrawDirtyRects
{
get => _renderer.DrawDirtyRects;
set => _renderer.DrawDirtyRects = value;
}
public event EventHandler<SceneInvalidatedEventArgs> SceneInvalidated
{
add => _renderer.SceneInvalidated += value;
remove => _renderer.SceneInvalidated -= value;
}
public void AddDirty(IVisual visual)
{
lock (_lock)
_invalidated = true;
_renderer.AddDirty(visual);
}
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter)
{
return _renderer.HitTest(p, root, filter);
}
public IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter)
{
return _renderer.HitTestFirst(p, root, filter);
}
public void RecalculateChildren(IVisual visual)
{
_renderer.RecalculateChildren(visual);
}
public void Resized(Size size)
{
_renderer.Resized(size);
}
public void Paint(Rect rect)
{
_invalidated = false;
_renderer.Paint(rect);
}
public void Start()
{
_loop.Add(this);
_renderer.Start();
}
public void Stop()
{
_loop.Remove(this);
_renderer.Stop();
}
public bool NeedsUpdate => false;
public void Update(TimeSpan time)
{
}
public void Render()
{
if (_invalidated)
{
lock (_lock)
_invalidated = false;
Dispatcher.UIThread.Post(() => Paint(new Rect(0, 0, 100000, 100000)));
}
}
}
}

1
src/Avalonia.X11/X11Platform.cs

@ -96,6 +96,7 @@ namespace Avalonia
public bool UseGpu { get; set; } = true;
public bool OverlayPopups { get; set; }
public bool UseDBusMenu { get; set; }
public bool UseDeferredRendering { get; set; } = true;
public List<string> GlxRendererBlacklist { get; set; } = new List<string>
{

29
src/Avalonia.X11/X11Window.cs

@ -27,7 +27,6 @@ namespace Avalonia.X11
private readonly IWindowImpl _popupParent;
private readonly bool _popup;
private readonly X11Info _x11;
private bool _invalidated;
private XConfigureEvent? _configure;
private PixelPoint? _configurePoint;
private bool _triggeredExpose;
@ -41,6 +40,7 @@ namespace Avalonia.X11
private IntPtr _xic;
private IntPtr _renderHandle;
private bool _mapped;
private bool _wasMappedAtLeastOnce = false;
private HashSet<X11Window> _transientChildren = new HashSet<X11Window>();
private X11Window _transientParent;
private double? _scalingOverride;
@ -308,8 +308,13 @@ namespace Avalonia.X11
public Action Closed { get; set; }
public Action<PixelPoint> PositionChanged { get; set; }
public IRenderer CreateRenderer(IRenderRoot root) =>
new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>());
public IRenderer CreateRenderer(IRenderRoot root)
{
var loop = AvaloniaLocator.Current.GetService<IRenderLoop>();
return _platform.Options.UseDeferredRendering ?
new DeferredRenderer(root, loop) :
(IRenderer)new X11ImmediateRendererProxy(root, loop);
}
void OnEvent(XEvent ev)
{
@ -683,20 +688,12 @@ namespace Avalonia.X11
void DoPaint()
{
_invalidated = false;
Paint?.Invoke(new Rect());
}
public void Invalidate(Rect rect)
{
if(_invalidated)
return;
_invalidated = true;
Dispatcher.UIThread.InvokeAsync(() =>
{
if (_mapped)
DoPaint();
});
}
public IInputRoot InputRoot => _inputRoot;
@ -777,6 +774,7 @@ namespace Avalonia.X11
void ShowCore()
{
_wasMappedAtLeastOnce = true;
XMapWindow(_x11.Display, _handle);
XFlush(_x11.Display);
}
@ -824,7 +822,7 @@ namespace Avalonia.X11
XConfigureResizeWindow(_x11.Display, _renderHandle, pixelSize);
XFlush(_x11.Display);
if (force || (_popup && needImmediatePopupResize))
if (force || !_wasMappedAtLeastOnce || (_popup && needImmediatePopupResize))
{
_realSize = pixelSize;
Resized?.Invoke(ClientSize);
@ -865,6 +863,11 @@ namespace Avalonia.X11
XConfigureWindow(_x11.Display, _handle, ChangeWindowFlags.CWX | ChangeWindowFlags.CWY,
ref changes);
XFlush(_x11.Display);
if (!_wasMappedAtLeastOnce)
{
_position = value;
PositionChanged?.Invoke(value);
}
}
}

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

@ -1,9 +1,5 @@
using System;
using System.Windows.Input;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Markup.Data;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
@ -159,6 +155,19 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Can_Set_Clear_ContextMenu_Property()
{
using (Application())
{
var target = new ContextMenu();
var control = new Panel();
control.ContextMenu = target;
control.ContextMenu = null;
}
}
[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()
{

78
tests/Avalonia.LeakTests/ControlTests.cs

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Contexts;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Diagnostics;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
@ -419,9 +421,83 @@ namespace Avalonia.LeakTests
}
}
[Fact]
public void Attached_ContextMenu_Is_Freed()
{
using (Start())
{
void AttachShowAndDetachContextMenu(Control control)
{
var contextMenu = new ContextMenu
{
Items = new[]
{
new MenuItem { Header = "Foo" },
new MenuItem { Header = "Foo" },
}
};
control.ContextMenu = contextMenu;
contextMenu.Open(control);
contextMenu.Close();
control.ContextMenu = null;
}
var window = new Window();
window.Show();
Assert.Same(window, FocusManager.Instance.Current);
AttachShowAndDetachContextMenu(window);
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount));
}
}
[Fact]
public void Standalone_ContextMenu_Is_Freed()
{
using (Start())
{
void BuildAndShowContextMenu(Control control)
{
var contextMenu = new ContextMenu
{
Items = new[]
{
new MenuItem { Header = "Foo" },
new MenuItem { Header = "Foo" },
}
};
contextMenu.Open(control);
contextMenu.Close();
}
var window = new Window();
window.Show();
Assert.Same(window, FocusManager.Instance.Current);
BuildAndShowContextMenu(window);
BuildAndShowContextMenu(window);
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount));
}
}
private IDisposable Start()
{
return UnitTestApplication.Start(TestServices.StyledWindow);
return UnitTestApplication.Start(TestServices.StyledWindow.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
inputManager: new InputManager()));
}
private class Node

4
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@ -21,6 +21,10 @@ namespace Avalonia.UnitTests
{
var win = Mock.Of<IWindowImpl>(x => x.Scaling == 1);
var mock = Mock.Get(win);
mock.Setup(x => x.Show()).Callback(() =>
{
mock.Object.Activated?.Invoke();
});
mock.Setup(x => x.CreatePopup()).Returns(() =>
{
if (popupImpl != null)

Loading…
Cancel
Save