Browse Source

Add support for Flyouts, ToolTips & ContextMenus

pull/6191/head
Luis von der Eltz 5 years ago
parent
commit
a9affd64bf
  1. 23
      src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs
  2. 12
      src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs
  3. 25
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  4. 40
      src/Avalonia.Controls/ToolTip.cs
  5. 11
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  6. 97
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

23
src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs

@ -0,0 +1,23 @@
using System;
using Avalonia.Controls.Primitives;
#nullable enable
namespace Avalonia.Controls.Diagnostics
{
/// <summary>
/// Diagnostics interface to retrieve an associated <see cref="IPopupHost"/>.
/// </summary>
public interface IPopupHostProvider
{
/// <summary>
/// The popup host.
/// </summary>
IPopupHost? PopupHost { get; }
/// <summary>
/// Raised when the popup host changes.
/// </summary>
event Action<IPopupHost?>? PopupHostChanged;
}
}

12
src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs

@ -0,0 +1,12 @@
#nullable enable
namespace Avalonia.Controls.Diagnostics
{
/// <summary>
/// Helper class to provide some diagnostics insides into <see cref="ToolTip"/>.
/// </summary>
public static class ToolTipDiagnostics
{
public static AvaloniaProperty<ToolTip?> ToolTipProperty = ToolTip.ToolTipProperty;
}
}

25
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<IPopupHost?>? _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<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
public event EventHandler? Closed;
public event EventHandler<CancelEventArgs>? 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)

40
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.
/// </remarks>
[PseudoClasses(":open")]
public class ToolTip : ContentControl
public class ToolTip : ContentControl, IPopupHostProvider
{
/// <summary>
/// Defines the ToolTip.Tip attached property.
@ -61,7 +60,8 @@ namespace Avalonia.Controls
internal static readonly AttachedProperty<ToolTip?> ToolTipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip?>("ToolTip");
private IPopupHost? _popup;
private IPopupHost? _popupHost;
private Action<IPopupHost?>? _popupHostChangedHandler;
/// <summary>
/// Initializes static members of the <see cref="ToolTip"/> class.
@ -251,35 +251,45 @@ namespace Avalonia.Controls
tooltip.RecalculatePosition(control);
}
IPopupHost? IPopupHostProvider.PopupHost => _popupHost;
event Action<IPopupHost?>? 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);
}
}

11
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

97
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<VisualTreeNode>();
return control is IVisual visual ?
new[] { new VisualTreeNode(visual, null) } :
Array.Empty<VisualTreeNode>();
}
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<TreeNode> nodes)
private static IObservable<IControl?>? GetHostedPopupRootObservable(IVisual visual)
{
if (_control is Popup p)
static IObservable<IControl?> 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<IPopupHost?>(
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<IControl?>(ContextMenu);
}
if ((ContextFlyout ?? (IPopupHostProvider?) AttachedFlyout ?? ToolTip) is { } popupHostProvider)
{
return GetPopupHostObservable(popupHostProvider);
}
return Observable.Return<IControl?>(null);
})
.Switch(),
_ => null
};
}
protected override void Initialize(AvaloniaList<TreeNode> 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()));
}
}
}

Loading…
Cancel
Save