diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index fc56e26add..1735599988 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -20,6 +20,8 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical }); private Popup _popup; + private Control _attachedControl; + private IInputElement _previousFocus; /// /// Initializes a new instance of the 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 /// The control. 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()) + 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) diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index bf811ad008..3715bc52a4 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/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; diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 196110edf7..63eabb32f4 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/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; diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 500677c545..bcae8a3c53 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/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; } diff --git a/src/Avalonia.X11/X11ImmediateRendererProxy.cs b/src/Avalonia.X11/X11ImmediateRendererProxy.cs new file mode 100644 index 0000000000..f7bc7f9711 --- /dev/null +++ b/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 SceneInvalidated + { + add => _renderer.SceneInvalidated += value; + remove => _renderer.SceneInvalidated -= value; + } + + public void AddDirty(IVisual visual) + { + lock (_lock) + _invalidated = true; + _renderer.AddDirty(visual); + } + + public IEnumerable HitTest(Point p, IVisual root, Func filter) + { + return _renderer.HitTest(p, root, filter); + } + + public IVisual HitTestFirst(Point p, IVisual root, Func 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))); + } + } + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 8b531bd9c5..028c47c978 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/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 GlxRendererBlacklist { get; set; } = new List { diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 2d124fff67..60fd0346a3 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/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 _transientChildren = new HashSet(); private X11Window _transientParent; private double? _scalingOverride; @@ -308,8 +308,13 @@ namespace Avalonia.X11 public Action Closed { get; set; } public Action PositionChanged { get; set; } - public IRenderer CreateRenderer(IRenderRoot root) => - new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); + public IRenderer CreateRenderer(IRenderRoot root) + { + var loop = AvaloniaLocator.Current.GetService(); + 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); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index f44f89e91f..5a47a86e51 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/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() { diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 5cec5c134d..0afb2465ee 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/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()).ObjectsCount)); + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).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()).ObjectsCount)); + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).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 diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index a6701ef655..782e4a0974 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -21,6 +21,10 @@ namespace Avalonia.UnitTests { var win = Mock.Of(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)