From a9affd64bf67d2ff39e3853b7f0adc168be71f3f Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 15 Jul 2021 15:46:47 +0200 Subject: [PATCH] Add support for Flyouts, ToolTips & ContextMenus --- .../Diagnostics/IPopupHostProvider.cs | 23 +++++ .../Diagnostics/ToolTipDiagnostics.cs | 12 +++ src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 25 ++++- src/Avalonia.Controls/ToolTip.cs | 40 +++++--- .../Diagnostics/ViewModels/TreeNode.cs | 11 ++- .../Diagnostics/ViewModels/VisualTreeNode.cs | 97 ++++++++++++++----- 6 files changed, 163 insertions(+), 45 deletions(-) create mode 100644 src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs create mode 100644 src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs diff --git a/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs b/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs new file mode 100644 index 0000000000..11d3b1792a --- /dev/null +++ b/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs @@ -0,0 +1,23 @@ +using System; +using Avalonia.Controls.Primitives; + +#nullable enable + +namespace Avalonia.Controls.Diagnostics +{ + /// + /// Diagnostics interface to retrieve an associated . + /// + public interface IPopupHostProvider + { + /// + /// The popup host. + /// + IPopupHost? PopupHost { get; } + + /// + /// Raised when the popup host changes. + /// + event Action? PopupHostChanged; + } +} diff --git a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs new file mode 100644 index 0000000000..58174b1039 --- /dev/null +++ b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs @@ -0,0 +1,12 @@ +#nullable enable + +namespace Avalonia.Controls.Diagnostics +{ + /// + /// Helper class to provide some diagnostics insides into . + /// + public static class ToolTipDiagnostics + { + public static AvaloniaProperty ToolTipProperty = ToolTip.ToolTipProperty; + } +} diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index e4b68c62fd..6b72b8c887 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -1,16 +1,16 @@ using System; using System.ComponentModel; +using Avalonia.Controls.Diagnostics; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Logging; -using Avalonia.Rendering; #nullable enable namespace Avalonia.Controls.Primitives { - public abstract class FlyoutBase : AvaloniaObject + public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider { static FlyoutBase() { @@ -55,6 +55,7 @@ namespace Avalonia.Controls.Primitives private Rect? _enlargedPopupRect; private PixelRect? _enlargePopupRectScreenPixelRect; private IDisposable? _transientDisposable; + private Action? _popupHostChangedHandler; protected Popup? Popup { get; private set; } @@ -94,6 +95,14 @@ namespace Avalonia.Controls.Primitives private set => SetAndRaise(TargetProperty, ref _target, value); } + IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; + + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; + remove => _popupHostChangedHandler -= value; + } + public event EventHandler? Closed; public event EventHandler? Closing; public event EventHandler? Opened; @@ -322,9 +331,11 @@ namespace Avalonia.Controls.Primitives private void InitPopup() { - Popup = new Popup(); - Popup.WindowManagerAddShadowHint = false; - Popup.IsLightDismissEnabled = true; + Popup = new Popup + { + WindowManagerAddShadowHint = false, + IsLightDismissEnabled = true + }; Popup.Opened += OnPopupOpened; Popup.Closed += OnPopupClosed; @@ -333,11 +344,15 @@ namespace Avalonia.Controls.Primitives private void OnPopupOpened(object sender, EventArgs e) { IsOpen = true; + + _popupHostChangedHandler?.Invoke(Popup!.Host); } private void OnPopupClosed(object sender, EventArgs e) { HideCore(); + + _popupHostChangedHandler?.Invoke(null); } private void PositionPopup(bool showAtPointer) diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index ab507d07a2..ab310d60ef 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -1,9 +1,8 @@ #nullable enable using System; -using System.Reactive.Linq; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -17,7 +16,7 @@ namespace Avalonia.Controls /// assigning the content that you want displayed. /// [PseudoClasses(":open")] - public class ToolTip : ContentControl + public class ToolTip : ContentControl, IPopupHostProvider { /// /// Defines the ToolTip.Tip attached property. @@ -61,7 +60,8 @@ namespace Avalonia.Controls internal static readonly AttachedProperty ToolTipProperty = AvaloniaProperty.RegisterAttached("ToolTip"); - private IPopupHost? _popup; + private IPopupHost? _popupHost; + private Action? _popupHostChangedHandler; /// /// Initializes static members of the class. @@ -251,35 +251,45 @@ namespace Avalonia.Controls tooltip.RecalculatePosition(control); } + + IPopupHost? IPopupHostProvider.PopupHost => _popupHost; + + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; + remove => _popupHostChangedHandler -= value; + } internal void RecalculatePosition(Control control) { - _popup?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); + _popupHost?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); } private void Open(Control control) { Close(); - _popup = OverlayPopupHost.CreatePopupHost(control, null); - _popup.SetChild(this); - ((ISetLogicalParent)_popup).SetParent(control); + _popupHost = OverlayPopupHost.CreatePopupHost(control, null); + _popupHost.SetChild(this); + ((ISetLogicalParent)_popupHost).SetParent(control); - _popup.ConfigurePosition(control, GetPlacement(control), + _popupHost.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); - WindowManagerAddShadowHintChanged(_popup, false); + WindowManagerAddShadowHintChanged(_popupHost, false); - _popup.Show(); + _popupHost.Show(); + _popupHostChangedHandler?.Invoke(_popupHost); } private void Close() { - if (_popup != null) + if (_popupHost != null) { - _popup.SetChild(null); - _popup.Dispose(); - _popup = null; + _popupHost.SetChild(null); + _popupHost.Dispose(); + _popupHost = null; + _popupHostChangedHandler?.Invoke(null); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index f4c04dbca6..9667751e54 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -4,6 +4,7 @@ using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.LogicalTree; using Avalonia.Media; @@ -19,12 +20,11 @@ namespace Avalonia.Diagnostics.ViewModels protected TreeNode(IVisual visual, TreeNode? parent) { + _classes = string.Empty; Parent = parent; Type = visual.GetType().Name; Visual = visual; - _classes = string.Empty; - - FontWeight = Visual is TopLevel || Visual is Popup ? FontWeight.Bold : FontWeight.Normal; + FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal; if (visual is IControl control) { @@ -56,6 +56,11 @@ namespace Avalonia.Diagnostics.ViewModels } } + private bool IsRoot => Visual is TopLevel || + Visual is Popup || + Visual is ContextMenu || + Visual is IPopupHost; + public FontWeight FontWeight { get; } public abstract TreeNodeCollection Children diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index b9981babf7..6d803a1a7b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,5 +1,10 @@ using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Security.Cryptography.X509Certificates; using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Styling; using Avalonia.VisualTree; @@ -13,7 +18,7 @@ namespace Avalonia.Diagnostics.ViewModels { Children = new VisualTreeNodeCollection(this, visual); - if ((Visual is IStyleable styleable)) + if (Visual is IStyleable styleable) { IsInTemplate = styleable.TemplatedParent != null; } @@ -25,13 +30,15 @@ namespace Avalonia.Diagnostics.ViewModels public static VisualTreeNode[] Create(object control) { - return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : Array.Empty(); + return control is IVisual visual ? + new[] { new VisualTreeNode(visual, null) } : + Array.Empty(); } internal class VisualTreeNodeCollection : TreeNodeCollection { private readonly IVisual _control; - private IDisposable? _subscription; + private readonly CompositeDisposable _subscriptions = new CompositeDisposable(2); public VisualTreeNodeCollection(TreeNode owner, IVisual control) : base(owner) @@ -41,33 +48,79 @@ namespace Avalonia.Diagnostics.ViewModels public override void Dispose() { - _subscription?.Dispose(); + _subscriptions.Dispose(); } - protected override void Initialize(AvaloniaList nodes) + private static IObservable? GetHostedPopupRootObservable(IVisual visual) { - if (_control is Popup p) + static IObservable GetPopupHostObservable(IPopupHostProvider popupHostProvider) { - _subscription = p.GetObservable(Popup.ChildProperty).Subscribe(child => - { - if (child != null) - { - nodes.Add(new VisualTreeNode(child, Owner)); - } - else - { - nodes.Clear(); - } - - }); + return Observable.FromEvent( + x => popupHostProvider.PopupHostChanged += x, + x => popupHostProvider.PopupHostChanged -= x) + .StartWith(popupHostProvider.PopupHost) + .Select(x => x is IControl c ? c : null); } - else + + return visual switch { - _subscription = _control.VisualChildren.ForEachItem( + Popup p => p.GetObservable(Popup.ChildProperty), + Control c => Observable.CombineLatest( + c.GetObservable(Control.ContextFlyoutProperty), + c.GetObservable(Control.ContextMenuProperty), + c.GetObservable(FlyoutBase.AttachedFlyoutProperty), + c.GetObservable(ToolTipDiagnostics.ToolTipProperty), + (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip) => + { + if (ContextMenu != null) + { + //Note: ContextMenus are special since all the items are added as visual children. + //So we don't need to go via Popup + return Observable.Return(ContextMenu); + } + + if ((ContextFlyout ?? (IPopupHostProvider?) AttachedFlyout ?? ToolTip) is { } popupHostProvider) + { + return GetPopupHostObservable(popupHostProvider); + } + + return Observable.Return(null); + }) + .Switch(), + _ => null + }; + } + + protected override void Initialize(AvaloniaList nodes) + { + _subscriptions.Clear(); + + if (GetHostedPopupRootObservable(_control) is { } popupRootObservable) + { + VisualTreeNode? childNode = null; + + _subscriptions.Add( + popupRootObservable + .Subscribe(root => + { + if (root != null) + { + childNode = new VisualTreeNode(root, Owner); + + nodes.Add(childNode); + } + else if (childNode != null) + { + nodes.Remove(childNode); + } + })); + } + + _subscriptions.Add( + _control.VisualChildren.ForEachItem( (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), (i, item) => nodes.RemoveAt(i), - () => nodes.Clear()); - } + () => nodes.Clear())); } } }