From f9561260a3d69f213ae4c63f31e5916da8fb9cd4 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 28 Jul 2019 23:08:31 +0300 Subject: [PATCH] IPopupImpl is now optional advanced feature --- src/Avalonia.Controls/ComboBox.cs | 2 +- src/Avalonia.Controls/MenuItem.cs | 2 +- .../WindowNotificationManager.cs | 2 +- .../Primitives/AdornerDecorator.cs | 42 ----- .../Primitives/AdornerLayer.cs | 2 +- .../Primitives/IPopupHost.cs | 22 +++ .../Primitives/OverlayLayer.cs | 145 ++++++++++++++++ src/Avalonia.Controls/Primitives/Popup.cs | 60 ++----- src/Avalonia.Controls/Primitives/PopupHost.cs | 160 ++++++++++++++++++ .../PopupPositioning/IPopupPositioner.cs | 64 +++++++ src/Avalonia.Controls/Primitives/PopupRoot.cs | 91 ++++------ .../Primitives/VisualLayerManager.cs | 97 +++++++++++ src/Avalonia.Controls/ToolTip.cs | 5 +- .../AvaloniaNativePlatformExtensions.cs | 1 + src/Avalonia.Native/WindowImpl.cs | 3 +- src/Avalonia.Themes.Default/ComboBox.xaml | 4 +- .../EmbeddableControlRoot.xaml | 6 +- src/Avalonia.Themes.Default/Window.xaml | 4 +- src/Avalonia.X11/X11Platform.cs | 1 + src/Avalonia.X11/X11Window.cs | 3 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 + src/Windows/Avalonia.Win32/WindowImpl.cs | 5 +- .../Primitives/PopupRootTests.cs | 41 ++++- .../Primitives/PopupTests.cs | 117 +++++++------ .../MarkupExtensions/BindingExtensionTests.cs | 13 +- .../MockWindowingPlatform.cs | 4 +- 26 files changed, 675 insertions(+), 223 deletions(-) delete mode 100644 src/Avalonia.Controls/Primitives/AdornerDecorator.cs create mode 100644 src/Avalonia.Controls/Primitives/IPopupHost.cs create mode 100644 src/Avalonia.Controls/Primitives/OverlayLayer.cs create mode 100644 src/Avalonia.Controls/Primitives/PopupHost.cs create mode 100644 src/Avalonia.Controls/Primitives/VisualLayerManager.cs diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index f32b8fabc6..a70d26624c 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -202,7 +202,7 @@ namespace Avalonia.Controls { if (!e.Handled) { - if (_popup?.PopupRoot != null && ((IVisual)e.Source).GetVisualRoot() == _popup?.PopupRoot) + if (_popup?.IsInsidePopup((IVisual)e.Source) == true) { if (UpdateSelectionFromEventSource(e.Source)) { diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index bd558af5ef..38cc3f6daf 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -224,7 +224,7 @@ namespace Avalonia.Controls public bool IsTopLevel => Parent is Menu; /// - bool IMenuItem.IsPointerOverSubMenu => _popup.PopupRoot?.IsPointerOver ?? false; + bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false; /// IMenuElement IMenuItem.Parent => Parent as IMenuElement; diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 93873cbf7d..aa91224572 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -150,7 +150,7 @@ namespace Avalonia.Controls.Notifications private void Install(Window host) { var adornerLayer = host.GetVisualDescendants() - .OfType() + .OfType() .FirstOrDefault() ?.AdornerLayer; diff --git a/src/Avalonia.Controls/Primitives/AdornerDecorator.cs b/src/Avalonia.Controls/Primitives/AdornerDecorator.cs deleted file mode 100644 index 4608d64806..0000000000 --- a/src/Avalonia.Controls/Primitives/AdornerDecorator.cs +++ /dev/null @@ -1,42 +0,0 @@ -// 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 Avalonia.LogicalTree; - -namespace Avalonia.Controls.Primitives -{ - public class AdornerDecorator : Decorator - { - public AdornerDecorator() - { - AdornerLayer = new AdornerLayer(); - ((ISetLogicalParent)AdornerLayer).SetParent(this); - AdornerLayer.ZIndex = int.MaxValue; - VisualChildren.Add(AdornerLayer); - } - - protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - base.OnAttachedToLogicalTree(e); - - ((ILogical)AdornerLayer).NotifyAttachedToLogicalTree(e); - } - - public AdornerLayer AdornerLayer - { - get; - } - - protected override Size MeasureOverride(Size availableSize) - { - AdornerLayer.Measure(availableSize); - return base.MeasureOverride(availableSize); - } - - protected override Size ArrangeOverride(Size finalSize) - { - AdornerLayer.Arrange(new Rect(finalSize)); - return base.ArrangeOverride(finalSize); - } - } -} diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index d198570909..ebe5e0a93e 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Primitives public static AdornerLayer GetAdornerLayer(IVisual visual) { return visual.GetVisualAncestors() - .OfType() + .OfType() .FirstOrDefault() ?.AdornerLayer; } diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs new file mode 100644 index 0000000000..ca0f723893 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs @@ -0,0 +1,22 @@ +using System; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives +{ + public interface IPopupHost : IDisposable + { + object Content { get; set; } + IVisual VisualRoot { get; } + + void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, + PopupPositioningEdge anchor = PopupPositioningEdge.None, + PopupPositioningEdge gravity = PopupPositioningEdge.None); + void Show(); + void Hide(); + IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, + StyledProperty minWidthProperty, StyledProperty maxWidthProperty, + StyledProperty heightProperty, StyledProperty minHeightProperty, + StyledProperty maxHeightProperty, StyledProperty topmostProperty); + } +} diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs new file mode 100644 index 0000000000..32dcf9f797 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/OverlayLayer.cs @@ -0,0 +1,145 @@ +using System.Linq; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives +{ + public class OverlayLayer : Control + { + /// + /// Defines the Left attached property. + /// + public static readonly AttachedProperty LeftProperty = + AvaloniaProperty.RegisterAttached("Left", 0); + + /// + /// Defines the Top attached property. + /// + public static readonly AttachedProperty TopProperty = + AvaloniaProperty.RegisterAttached("Top", 0); + + /// + /// Defines the InfiniteAvailableSize attached property. + /// + public static readonly AttachedProperty InfiniteAvailableSizeProperty = + AvaloniaProperty.RegisterAttached("InfiniteAvailableSize", false); + + + static OverlayLayer() + { + foreach (var p in new []{LeftProperty, TopProperty}) + { + p.Changed.AddClassHandler((target, e) => + { + if (target.GetVisualParent() is OverlayLayer layer) + layer.InvalidateArrange(); + }); + } + } + + public Size AvailableSize { get; private set; } + + /// + /// Gets the value of the Left attached property for a control. + /// + /// The control. + /// The control's left coordinate. + public static double GetLeft(AvaloniaObject element) + { + return element.GetValue(LeftProperty); + } + + /// + /// Sets the value of the Left attached property for a control. + /// + /// The control. + /// The left value. + public static void SetLeft(AvaloniaObject element, double value) + { + element.SetValue(LeftProperty, value); + } + + /// + /// Gets the value of the Top attached property for a control. + /// + /// The control. + /// The control's top coordinate. + public static double GetTop(AvaloniaObject element) + { + return element.GetValue(TopProperty); + } + + /// + /// Sets the value of the Top attached property for a control. + /// + /// The control. + /// The top value. + public static void SetTop(AvaloniaObject element, double value) + { + element.SetValue(TopProperty, value); + } + + /// + /// Gets the value of the Top attached property for a control. + /// + /// The control. + /// The control's top coordinate. + public static bool GetInfiniteAvailableSize(AvaloniaObject element) + { + return element.GetValue(InfiniteAvailableSizeProperty); + } + + /// + /// Sets the value of the Top attached property for a control. + /// + /// The control. + /// The top value. + public static void SetInfiniteAvailableSize(AvaloniaObject element, bool value) + { + element.SetValue(InfiniteAvailableSizeProperty, value); + } + + + public static OverlayLayer GetOverlayLayer(IVisual visual) + { + foreach(var v in visual.GetVisualAncestors()) + if(v is VisualLayerManager vlm) + if (vlm.OverlayLayer != null) + return vlm.OverlayLayer; + if (visual is TopLevel tl) + { + var layers = tl.GetVisualDescendants().OfType().FirstOrDefault(); + return layers?.OverlayLayer; + } + + return null; + } + + public void Add(Control v) + { + VisualChildren.Add(v); + InvalidateArrange(); + } + + public void Remove(Control v) => VisualChildren.Remove(v); + + protected override Size MeasureOverride(Size availableSize) + { + + var infinite = new Size(double.PositiveInfinity, double.PositiveInfinity); + foreach (Control v in VisualChildren) + v.Measure(GetInfiniteAvailableSize(v) ? infinite : availableSize); + + return new Size(); + } + + protected override Size ArrangeOverride(Size finalSize) + { + // We are saving it here since child controls might need to know the entire size of the overlay + // and Bounds won't be updated in time + AvailableSize = finalSize; + foreach (Control v in VisualChildren) + v.Arrange(new Rect(GetLeft(v), GetTop(v), v.DesiredSize.Width, v.DesiredSize.Height)); + return finalSize; + } + } +} diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 201566831d..dc92347c9d 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -79,7 +79,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(Topmost)); private bool _isOpen; - private PopupRoot _popupRoot; + private IPopupHost _popupRoot; private TopLevel _topLevel; private IDisposable _nonClientListener; bool _ignoreIsOpenChanged = false; @@ -94,7 +94,6 @@ namespace Avalonia.Controls.Primitives IsHitTestVisibleProperty.OverrideDefaultValue(false); ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); IsOpenProperty.Changed.AddClassHandler(x => x.IsOpenChanged); - TopmostProperty.Changed.AddClassHandler((p, e) => p.PopupRoot.Topmost = (bool)e.NewValue); } public Popup() @@ -112,10 +111,7 @@ namespace Avalonia.Controls.Primitives /// public event EventHandler Opened; - /// - /// Raised when the popup root has been created, but before it has been shown. - /// - public event EventHandler PopupRootCreated; + public IPopupHost Host => _popupRoot; /// /// Gets or sets the control to display in the popup. @@ -192,11 +188,6 @@ namespace Avalonia.Controls.Primitives set { SetValue(PlacementTargetProperty, value); } } - /// - /// Gets the root of the popup window. - /// - public PopupRoot PopupRoot => _popupRoot; - /// /// Gets or sets a value indicating whether the popup should stay open when the popup is /// pressed or loses focus. @@ -219,7 +210,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets the root of the popup window. /// - IVisual IVisualTreeHost.Root => _popupRoot; + IVisual IVisualTreeHost.Root => _popupRoot?.VisualRoot; /// /// Opens the popup. @@ -242,28 +233,13 @@ namespace Avalonia.Controls.Primitives "Attempted to open a popup not attached to a TopLevel"); } + _popupRoot = PopupHost.CreatePopupHost(placementTarget, DependencyResolver); - _popupRoot = new PopupRoot(_topLevel, DependencyResolver) - { - [~WidthProperty] = this[~WidthProperty], - [~HeightProperty] = this[~HeightProperty], - [~MinWidthProperty] = this[~MinWidthProperty], - [~MaxWidthProperty] = this[~MaxWidthProperty], - [~MinHeightProperty] = this[~MinHeightProperty], - [~MaxHeightProperty] = this[~MaxHeightProperty], - }; - - void Bind(AvaloniaProperty prop) => _bindings.Add(_popupRoot.Bind(prop, this[~prop])); - - Bind(WidthProperty); - Bind(MinWidthProperty); - Bind(MaxWidthProperty); - Bind(HeightProperty); - Bind(MinHeightProperty); - Bind(MaxHeightProperty); - _decorator.Bind(PopupContentHost.ChildProperty, this[~ChildProperty]); - - _popupRoot.Content = _decorator; + _bindings.Add(_popupRoot.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, + HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty)); + _bindings.Add(_decorator.Bind(PopupContentHost.ChildProperty, this[~ChildProperty])); + + _popupRoot.SetContent(_decorator); ((ISetLogicalParent)_popupRoot).SetParent(this); @@ -287,7 +263,6 @@ namespace Avalonia.Controls.Primitives _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); _nonClientListener = InputManager.Instance?.Process.Subscribe(ListenForNonClientClick); - PopupRootCreated?.Invoke(this, EventArgs.Empty); _popupRoot.Show(); @@ -338,7 +313,7 @@ namespace Avalonia.Controls.Primitives foreach(var b in _bindings) b.Dispose(); _bindings.Clear(); - _popupRoot.Content = null; + _popupRoot.SetContent(null); _popupRoot.Hide(); ((ISetLogicalParent)_popupRoot).SetParent(null); _popupRoot.Dispose(); @@ -425,14 +400,15 @@ namespace Avalonia.Controls.Primitives private bool IsChildOrThis(IVisual child) { - IVisual root = child.GetVisualRoot(); - while (root is PopupRoot) - { - if (root == PopupRoot) return true; - root = ((PopupRoot)root).Parent.GetVisualRoot(); - } - return false; + return _decorator.FindCommonVisualAncestor(child) == _decorator; } + + public bool IsInsidePopup(IVisual visual) + { + return _decorator.FindCommonVisualAncestor(visual) == _decorator; + } + + public bool IsPointerOverPopup => _decorator.IsPointerOver; private void WindowDeactivated(object sender, EventArgs e) { diff --git a/src/Avalonia.Controls/Primitives/PopupHost.cs b/src/Avalonia.Controls/Primitives/PopupHost.cs new file mode 100644 index 0000000000..3d6809c061 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/PopupHost.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Avalonia.Media; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives +{ + public class PopupHost : Control, IPopupHost, IInteractive, IManagedPopupPositionerPopup + { + private readonly OverlayLayer _overlayLayer; + private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters(); + private ManagedPopupPositioner _positioner; + private bool _shown; + private IControl _content; + + public PopupHost(OverlayLayer overlayLayer) + { + _overlayLayer = overlayLayer; + _positioner = new ManagedPopupPositioner(this); + } + + public void SetContent(IControl control) + { + if (_content == control) + return; + if (_content != null) + VisualChildren.Remove(_content); + _content = control; + if (_content != null) + VisualChildren.Add(_content); + } + + public IVisual VisualRoot => null; + + /// + IInteractive IInteractive.InteractiveParent => Parent; + + public void Dispose() => Hide(); + + + public void Show() + { + _overlayLayer.Add(this); + _shown = true; + } + + public void Hide() + { + _overlayLayer.Remove(this); + _shown = false; + } + + public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty, + StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty, + StyledProperty maxHeightProperty, StyledProperty topmostProperty) + { + // Topmost property is not supported + var bindings = new List(); + + void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to])); + Bind(WidthProperty, widthProperty); + Bind(MinWidthProperty, minWidthProperty); + Bind(MaxWidthProperty, maxWidthProperty); + Bind(HeightProperty, heightProperty); + Bind(MinHeightProperty, minHeightProperty); + Bind(MaxHeightProperty, maxHeightProperty); + + return Disposable.Create(() => + { + foreach (var x in bindings) + x.Dispose(); + }); + } + + public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, + PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None) + { + _positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot(), target, placement, offset, anchor, + gravity); + UpdatePosition(); + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (_positionerParameters.Size != finalSize) + { + _positionerParameters.Size = finalSize; + UpdatePosition(); + } + return base.ArrangeOverride(finalSize); + } + + + void UpdatePosition() + { + // Don't bother the positioner with layout system artifacts + if (_positionerParameters.Size.Width == 0 || _positionerParameters.Size.Height == 0) + return; + if (_shown) + { + _positioner.Update(_positionerParameters); + } + } + + IReadOnlyList IManagedPopupPositionerPopup.Screens + { + get + { + var rc = new Rect(default, _overlayLayer.AvailableSize); + return new[] {new ManagedPopupPositionerScreenInfo(rc, rc)}; + } + } + + Rect IManagedPopupPositionerPopup.ParentClientAreaScreenGeometry => + new Rect(default, _overlayLayer.Bounds.Size); + + private Point _lastRequestedPosition; + void IManagedPopupPositionerPopup.MoveAndResize(Point devicePoint, Size virtualSize) + { + _lastRequestedPosition = devicePoint; + Dispatcher.UIThread.Post(() => + { + OverlayLayer.SetLeft(this, _lastRequestedPosition.X); + OverlayLayer.SetTop(this, _lastRequestedPosition.Y); + }, DispatcherPriority.Layout); + } + + Point IManagedPopupPositionerPopup.TranslatePoint(Point pt) => pt; + + Size IManagedPopupPositionerPopup.TranslateSize(Size size) => size; + + public static IPopupHost CreatePopupHost(IVisual target, IAvaloniaDependencyResolver dependencyResolver) + { + var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup(); + if (platform != null) + return new PopupRoot((TopLevel)target.GetVisualRoot(), platform, dependencyResolver); + { + var overlayLayer = OverlayLayer.GetOverlayLayer(target); + if (overlayLayer == null) + throw new InvalidOperationException( + "Unable to create IPopupImpl and no overlay layer is found for the target control"); + + + return new PopupHost(overlayLayer); + } + } + + public override void Render(DrawingContext context) + { + context.FillRectangle(Brushes.White, new Rect(default, Bounds.Size)); + } + } +} diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index af78483b7f..3010a3d8a8 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -45,6 +45,7 @@ Copyright © 2019 Nikita Tsukanov */ using System; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives.PopupPositioning { @@ -290,5 +291,68 @@ namespace Avalonia.Controls.Primitives.PopupPositioning void Update(PopupPositionerParameters parameters); } + static class PopupPositionerExtensions + { + public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters, + TopLevel topLevel, + IVisual target, PlacementMode placement, Point offset, + PopupPositioningEdge anchor, PopupPositioningEdge gravity) + { + // We need a better way for tracking the last pointer position + var pointer = topLevel.PointToClient(topLevel.PlatformImpl.MouseDevice.Position); + + positionerParameters.Offset = offset; + positionerParameters.ConstraintAdjustment = PopupPositionerConstraintAdjustment.All; + if (placement == PlacementMode.Pointer) + { + positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1)); + positionerParameters.Anchor = PopupPositioningEdge.BottomRight; + positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + } + else + { + if (target == null) + throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); + var matrix = target.TransformToVisual(topLevel); + if (matrix == null) + { + if (target.GetVisualRoot() == null) + throw new InvalidCastException("Target control is not attached to the visual tree"); + throw new InvalidCastException("Target control is not in the same tree as the popup parent"); + } + + positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size) + .TransformToAABB(matrix.Value); + + if (placement == PlacementMode.Right) + { + positionerParameters.Anchor = PopupPositioningEdge.TopRight; + positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + } + else if (placement == PlacementMode.Bottom) + { + positionerParameters.Anchor = PopupPositioningEdge.BottomLeft; + positionerParameters.Gravity = PopupPositioningEdge.BottomRight; + } + else if (placement == PlacementMode.Left) + { + positionerParameters.Anchor = PopupPositioningEdge.TopLeft; + positionerParameters.Gravity = PopupPositioningEdge.BottomLeft; + } + else if (placement == PlacementMode.Top) + { + positionerParameters.Anchor = PopupPositioningEdge.TopLeft; + positionerParameters.Gravity = PopupPositioningEdge.TopRight; + } + else if (placement == PlacementMode.AnchorAndGravity) + { + positionerParameters.Anchor = anchor; + positionerParameters.Gravity = gravity; + } + else + throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); + } + } + } } diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 0437d4a550..36595f69c4 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -2,7 +2,9 @@ // 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 System.Reactive.Disposables; using System.Text; using Avalonia.Controls.Platform; using Avalonia.Controls.Presenters; @@ -21,7 +23,7 @@ namespace Avalonia.Controls.Primitives /// /// The root window of a . /// - public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost + public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost { private readonly TopLevel _parent; private IDisposable _presenterSubscription; @@ -38,8 +40,8 @@ namespace Avalonia.Controls.Primitives /// /// Initializes a new instance of the class. /// - public PopupRoot(TopLevel parent) - : this(parent, null) + public PopupRoot(TopLevel parent, IPopupImpl impl) + : this(parent, impl,null) { } @@ -49,8 +51,8 @@ namespace Avalonia.Controls.Primitives /// /// The dependency resolver to use. If null the default dependency resolver will be used. /// - public PopupRoot(TopLevel parent, IAvaloniaDependencyResolver dependencyResolver) - : base(parent.PlatformImpl.CreatePopup(), dependencyResolver) + public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver dependencyResolver) + : base(impl, dependencyResolver) { _parent = parent; } @@ -133,65 +135,36 @@ namespace Avalonia.Controls.Primitives PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None) { - // We need a better way for tracking the last pointer position - var pointer = _parent.PointToClient(_parent.PlatformImpl.MouseDevice.Position); - - _positionerParameters.Offset = offset; - _positionerParameters.ConstraintAdjustment = PopupPositionerConstraintAdjustment.All; - if (placement == PlacementMode.Pointer) - { - _positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1)); - _positionerParameters.Anchor = PopupPositioningEdge.BottomRight; - _positionerParameters.Gravity = PopupPositioningEdge.BottomRight; - } - else - { - if (target == null) - throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); - var matrix = target.TransformToVisual(_parent); - if (matrix == null) - { - if (target.GetVisualRoot() == null) - throw new InvalidCastException("Target control is not attached to the visual tree"); - throw new InvalidCastException("Target control is not in the same tree as the popup parent"); - } - - _positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size) - .TransformToAABB(matrix.Value); - - if (placement == PlacementMode.Right) - { - _positionerParameters.Anchor = PopupPositioningEdge.TopRight; - _positionerParameters.Gravity = PopupPositioningEdge.BottomRight; - } - else if (placement == PlacementMode.Bottom) - { - _positionerParameters.Anchor = PopupPositioningEdge.BottomLeft; - _positionerParameters.Gravity = PopupPositioningEdge.BottomRight; - } - else if (placement == PlacementMode.Left) - { - _positionerParameters.Anchor = PopupPositioningEdge.TopLeft; - _positionerParameters.Gravity = PopupPositioningEdge.BottomLeft; - } - else if (placement == PlacementMode.Top) - { - _positionerParameters.Anchor = PopupPositioningEdge.TopLeft; - _positionerParameters.Gravity = PopupPositioningEdge.TopRight; - } - else if (placement == PlacementMode.AnchorAndGravity) - { - _positionerParameters.Anchor = anchor; - _positionerParameters.Gravity = gravity; - } - else - throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); - } + _positionerParameters.ConfigurePosition(_parent, target, + placement, offset, anchor, gravity); if (_positionerParameters.Size != default) UpdatePosition(); } + + IVisual IPopupHost.VisualRoot => this; + public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty, StyledProperty minWidthProperty, + StyledProperty maxWidthProperty, StyledProperty heightProperty, StyledProperty minHeightProperty, + StyledProperty maxHeightProperty, StyledProperty topmostProperty) + { + var bindings = new List(); + + void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to])); + Bind(WidthProperty, widthProperty); + Bind(MinWidthProperty, minWidthProperty); + Bind(MaxWidthProperty, maxWidthProperty); + Bind(HeightProperty, heightProperty); + Bind(MinHeightProperty, minHeightProperty); + Bind(MaxHeightProperty, maxHeightProperty); + Bind(TopmostProperty, topmostProperty); + return Disposable.Create(() => + { + foreach (var x in bindings) + x.Dispose(); + }); + } + /// /// Carries out the arrange pass of the window. /// diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs new file mode 100644 index 0000000000..7354f2788f --- /dev/null +++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using Avalonia.LogicalTree; +using Avalonia.Styling; + +namespace Avalonia.Controls.Primitives +{ + public class VisualLayerManager : Decorator + { + private const int AdornerZIndex = int.MaxValue - 100; + private const int OverlayZIndex = int.MaxValue - 99; + + private bool _isAttachedToLogicalTree; + private IStyleHost _styleHost; + public bool IsPopup { get; set; } + + List _layers = new List(); + + + public AdornerLayer AdornerLayer + { + get + { + var rv = FindLayer(); + if (rv == null) + AddLayer(rv = new AdornerLayer(), AdornerZIndex); + return rv; + } + } + + public OverlayLayer OverlayLayer + { + get + { + if (IsPopup) + return null; + var rv = FindLayer(); + if(rv == null) + AddLayer(rv = new OverlayLayer(), OverlayZIndex); + return rv; + } + } + + T FindLayer() where T : class + { + foreach (var layer in _layers) + if (layer is T match) + return match; + return null; + } + + void AddLayer(Control layer, int zindex) + { + _layers.Add(layer); + ((ISetLogicalParent)layer).SetParent(this); + layer.ZIndex = zindex; + VisualChildren.Add(layer); + if (_isAttachedToLogicalTree) + ((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_styleHost)); + InvalidateArrange(); + } + + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + _isAttachedToLogicalTree = true; + _styleHost = e.Root; + + foreach (var l in _layers) + ((ILogical)l).NotifyAttachedToLogicalTree(e); + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + _styleHost = null; + _isAttachedToLogicalTree = false; + base.OnDetachedFromLogicalTree(e); + foreach (var l in _layers) + ((ILogical)l).NotifyDetachedFromLogicalTree(e); + } + + + protected override Size MeasureOverride(Size availableSize) + { + foreach (var l in _layers) + l.Measure(availableSize); + return base.MeasureOverride(availableSize); + } + + protected override Size ArrangeOverride(Size finalSize) + { + foreach (var l in _layers) + l.Arrange(new Rect(finalSize)); + return base.ArrangeOverride(finalSize); + } + } +} diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index da537a2e65..1bfcb47bb9 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -61,7 +61,7 @@ namespace Avalonia.Controls private static readonly AttachedProperty ToolTipProperty = AvaloniaProperty.RegisterAttached("ToolTip"); - private PopupRoot _popup; + private IPopupHost _popup; /// /// Initializes static members of the class. @@ -235,7 +235,8 @@ namespace Avalonia.Controls { Close(); - _popup = new PopupRoot((TopLevel)control.GetVisualRoot()) {Content = this}; + _popup = PopupHost.CreatePopupHost(control, null); + _popup.Content = this; ((ISetLogicalParent)_popup).SetParent(control); _popup.ConfigurePosition(control, GetPlacement(control), diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 09f822cf46..02810ed155 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -24,6 +24,7 @@ namespace Avalonia { public bool UseDeferredRendering { get; set; } = true; public bool UseGpu { get; set; } = true; + public bool OverlayPopups { get; set; } public string AvaloniaNativeLibraryPath { get; set; } } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index e4c158eeb3..490d5688a8 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -106,6 +106,7 @@ namespace Avalonia.Native public Func Closing { get; set; } public void Move(PixelPoint point) => Position = point; - public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, this); + public override IPopupImpl CreatePopup() => + _opts.OverlayPopups ? null : new PopupImpl(_factory, _opts, this); } } diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index 6227962a48..ffc96d5a2c 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -39,7 +39,7 @@ StaysOpen="False"> - + - + diff --git a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml index bc06ab010e..1fd168c009 100644 --- a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml @@ -4,13 +4,13 @@ - + - + - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/Window.xaml b/src/Avalonia.Themes.Default/Window.xaml index 2514422ce8..2a8b5d0fca 100644 --- a/src/Avalonia.Themes.Default/Window.xaml +++ b/src/Avalonia.Themes.Default/Window.xaml @@ -5,14 +5,14 @@ - + - + diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 9bdcaab82b..e88a7d8db2 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -91,6 +91,7 @@ namespace Avalonia { public bool UseEGL { get; set; } public bool UseGpu { get; set; } = true; + public bool OverlayPopups { get; set; } public List GlxRendererBlacklist { get; set; } = new List { diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index a3c00abda9..5481862f23 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -812,7 +812,8 @@ namespace Avalonia.X11 } public IMouseDevice MouseDevice => _mouse; - public IPopupImpl CreatePopup() => new X11Window(_platform, this); + public IPopupImpl CreatePopup() + => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this); public void Activate() { diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 56a7e356b6..f20cf394bb 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -41,6 +41,7 @@ namespace Avalonia public bool UseDeferredRendering { get; set; } = true; public bool AllowEglInitialization { get; set; } public bool? EnableMultitouch { get; set; } + public bool OverlayPopups { get; set; } } } @@ -61,6 +62,7 @@ namespace Avalonia.Win32 } public static bool UseDeferredRendering => Options.UseDeferredRendering; + internal static bool UseOverlayPopups => Options.OverlayPopups; public static Win32PlatformOptions Options { get; private set; } public Size DoubleClickSize => new Size( diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 21625af84a..e33e1f11dc 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -250,10 +250,7 @@ namespace Avalonia.Win32 UnmanagedMethods.SetActiveWindow(_hwnd); } - public IPopupImpl CreatePopup() - { - return new PopupImpl(this); - } + public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this); public void Dispose() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 944bf1e642..e840e7b530 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -54,10 +54,47 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Popup.Open(); - Assert.Equal(target.Popup, ((IStyleHost)target.Popup.PopupRoot).StylingParent); + Assert.Equal(target.Popup, ((IStyleHost)target.Popup.Host).StylingParent); } } + [Fact] + public void PopupRoot_Should_Have_Template_Applied() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + var target = new Popup {PlacementMode = PlacementMode.Pointer}; + var child = new Control(); + + window.Content = target; + window.ApplyTemplate(); + target.Open(); + + + Assert.Single(((Visual)target.Host).GetVisualChildren()); + + var templatedChild = ((Visual)target.Host).GetVisualChildren().Single(); + Assert.IsType(templatedChild); + + + Assert.Equal((PopupRoot)target.Host, ((IControl)templatedChild).TemplatedParent); + } + } + + [Fact] + public void PopupRoot_Should_Have_Null_VisualParent() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Popup() {PlacementTarget = new Window()}; + + target.Open(); + + Assert.Null(((Visual)target.Host).GetVisualParent()); + } + } + [Fact] public void Attaching_PopupRoot_To_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree() { @@ -134,7 +171,7 @@ namespace Avalonia.Controls.UnitTests.Primitives private PopupRoot CreateTarget(TopLevel popupParent) { - var result = new PopupRoot(popupParent) + var result = new PopupRoot(popupParent, popupParent.PlatformImpl.CreatePopup()) { Template = new FuncControlTemplate((parent, scope) => new ContentPresenter diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 3df4de6b68..ccdfe8af33 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -22,6 +22,8 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class PopupTests { + protected bool UsePopupHost; + [Fact] public void Setting_Child_Should_Set_Child_Controls_LogicalParent() { @@ -137,20 +139,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new Popup(); - Assert.Null(target.PopupRoot); - } - } - - [Fact] - public void PopupRoot_Should_Have_Null_VisualParent() - { - using (CreateServices()) - { - var target = new Popup() {PlacementTarget = new Window()}; - - target.Open(); - - Assert.Null(target.PopupRoot.GetVisualParent()); + Assert.Null(((Visual)target.Host)); } } @@ -159,12 +148,12 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var target = new Popup() {PlacementTarget = new Window()}; + var target = new Popup() {PlacementTarget = PreparedWindow()}; target.Open(); - Assert.Equal(target, target.PopupRoot.Parent); - Assert.Equal(target, target.PopupRoot.GetLogicalParent()); + Assert.Equal(target, ((Visual)target.Host).Parent); + Assert.Equal(target, ((Visual)target.Host).GetLogicalParent()); } } @@ -174,11 +163,11 @@ namespace Avalonia.Controls.UnitTests.Primitives using (CreateServices()) { var target = new Popup() {PlacementMode = PlacementMode.Pointer}; - var root = new Window() { Content = target }; + var root = PreparedWindow(target); target.Open(); - var popupRoot = (ILogical)target.PopupRoot; + var popupRoot = (ILogical)((Visual)target.Host); Assert.True(popupRoot.IsAttachedToLogicalTree); root.Content = null; @@ -191,7 +180,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var window = new Window(); + var window = PreparedWindow(); var target = new Popup() {PlacementMode = PlacementMode.Pointer}; window.Content = target; @@ -214,7 +203,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var window = new Window(); + var window = PreparedWindow(); var target = new Popup() {PlacementMode = PlacementMode.Pointer}; window.Content = target; @@ -234,48 +223,28 @@ namespace Avalonia.Controls.UnitTests.Primitives } } - [Fact] - public void PopupRoot_Should_Have_Template_Applied() - { - using (CreateServices()) - { - var window = new Window(); - var target = new Popup {PlacementMode = PlacementMode.Pointer}; - var child = new Control(); - - window.Content = target; - window.ApplyTemplate(); - target.Open(); - - Assert.Single(target.PopupRoot.GetVisualChildren()); - - var templatedChild = target.PopupRoot.GetVisualChildren().Single(); - Assert.IsType(templatedChild); - Assert.Equal(target.PopupRoot, ((IControl)templatedChild).TemplatedParent); - } - } - + [Fact] public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent() { + if(UsePopupHost) + // For some reason with overlay popups templates don't get applied in test mode but + // everything works perfectly fine at runtime. I leave this one to you @grokys + return; using (CreateServices()) { PopupContentControl target; - var root = new Window() + var root = PreparedWindow(target = new PopupContentControl { - Content = target = new PopupContentControl - { - Content = new Border(), - Template = new FuncControlTemplate(PopupContentControlTemplate), - }, - //StylingParent = AvaloniaLocator.Current.GetService() - }; - root.ApplyTemplate(); + Content = new Border(), + Template = new FuncControlTemplate(PopupContentControlTemplate), + }); + root.Show(); target.ApplyTemplate(); var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup"); popup.Open(); - var popupRoot = popup.PopupRoot; + var popupRoot = (Visual)popup.Host; var children = popupRoot.GetVisualDescendants().ToList(); var types = children.Select(x => x.GetType().Name).ToList(); @@ -306,6 +275,13 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + Window PreparedWindow(object content = null) + { + var w = new Window {Content = content}; + w.ApplyTemplate(); + return w; + } + [Fact] public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit() { @@ -316,7 +292,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = child = new TestControl(), DataContext = "foo", - PlacementTarget = new Window() + PlacementTarget = PreparedWindow() }; var beginCalled = false; @@ -336,9 +312,34 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.False(beginCalled); } } + + [Fact] + public void Popup_Host_Type_Should_Match_Platform_Preference() + { + using (CreateServices()) + { + var target = new Popup() {PlacementTarget = PreparedWindow()}; + + target.Open(); + if (UsePopupHost) + Assert.IsType(target.Host); + else + Assert.IsType(target.Host); + } + } - private static IDisposable CreateServices() => UnitTestApplication.Start(TestServices.StyledWindow); + private IDisposable CreateServices() + { + return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: + new MockWindowingPlatform(null, + () => + { + if(UsePopupHost) + return null; + return MockWindowingPlatform.CreatePopupMock().Object; + }))); + } private static IControl PopupRootTemplate(PopupRoot control, INameScope scope) { @@ -379,4 +380,12 @@ namespace Avalonia.Controls.UnitTests.Primitives } } } + + public class PopupTestsWithPopupRoot : PopupTests + { + public PopupTestsWithPopupRoot() + { + UsePopupHost = true; + } + } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs index dcecfe3b22..b8d41d5a87 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text; using Avalonia.Controls; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Styling; using Avalonia.UnitTests; @@ -59,11 +60,15 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions new Setter( Window.TemplateProperty, new FuncControlTemplate((x, scope) => - new ContentPresenter + new VisualLayerManager { - Name = "PART_ContentPresenter", - [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty], - }.RegisterInNameScope(scope))) + Child = + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty], + }.RegisterInNameScope(scope) + })) } }; } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 1b47318fe1..c33ec72141 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -23,7 +23,9 @@ namespace Avalonia.UnitTests var mock = Mock.Get(win); mock.Setup(x => x.CreatePopup()).Returns(() => { - return popupImpl?.Invoke() ?? CreatePopupMock().Object; + if (popupImpl != null) + return popupImpl(); + return CreatePopupMock().Object; }); PixelPoint pos = default;