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;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Controls.Diagnostics;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Rendering;
#nullable enable #nullable enable
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
public abstract class FlyoutBase : AvaloniaObject public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider
{ {
static FlyoutBase() static FlyoutBase()
{ {
@ -55,6 +55,7 @@ namespace Avalonia.Controls.Primitives
private Rect? _enlargedPopupRect; private Rect? _enlargedPopupRect;
private PixelRect? _enlargePopupRectScreenPixelRect; private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable; private IDisposable? _transientDisposable;
private Action<IPopupHost?>? _popupHostChangedHandler;
protected Popup? Popup { get; private set; } protected Popup? Popup { get; private set; }
@ -94,6 +95,14 @@ namespace Avalonia.Controls.Primitives
private set => SetAndRaise(TargetProperty, ref _target, value); 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? Closed;
public event EventHandler<CancelEventArgs>? Closing; public event EventHandler<CancelEventArgs>? Closing;
public event EventHandler? Opened; public event EventHandler? Opened;
@ -322,9 +331,11 @@ namespace Avalonia.Controls.Primitives
private void InitPopup() private void InitPopup()
{ {
Popup = new Popup(); Popup = new Popup
Popup.WindowManagerAddShadowHint = false; {
Popup.IsLightDismissEnabled = true; WindowManagerAddShadowHint = false,
IsLightDismissEnabled = true
};
Popup.Opened += OnPopupOpened; Popup.Opened += OnPopupOpened;
Popup.Closed += OnPopupClosed; Popup.Closed += OnPopupClosed;
@ -333,11 +344,15 @@ namespace Avalonia.Controls.Primitives
private void OnPopupOpened(object sender, EventArgs e) private void OnPopupOpened(object sender, EventArgs e)
{ {
IsOpen = true; IsOpen = true;
_popupHostChangedHandler?.Invoke(Popup!.Host);
} }
private void OnPopupClosed(object sender, EventArgs e) private void OnPopupClosed(object sender, EventArgs e)
{ {
HideCore(); HideCore();
_popupHostChangedHandler?.Invoke(null);
} }
private void PositionPopup(bool showAtPointer) private void PositionPopup(bool showAtPointer)

40
src/Avalonia.Controls/ToolTip.cs

@ -1,9 +1,8 @@
#nullable enable #nullable enable
using System; using System;
using System.Reactive.Linq; using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -17,7 +16,7 @@ namespace Avalonia.Controls
/// assigning the content that you want displayed. /// assigning the content that you want displayed.
/// </remarks> /// </remarks>
[PseudoClasses(":open")] [PseudoClasses(":open")]
public class ToolTip : ContentControl public class ToolTip : ContentControl, IPopupHostProvider
{ {
/// <summary> /// <summary>
/// Defines the ToolTip.Tip attached property. /// Defines the ToolTip.Tip attached property.
@ -61,7 +60,8 @@ namespace Avalonia.Controls
internal static readonly AttachedProperty<ToolTip?> ToolTipProperty = internal static readonly AttachedProperty<ToolTip?> ToolTipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip?>("ToolTip"); AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip?>("ToolTip");
private IPopupHost? _popup; private IPopupHost? _popupHost;
private Action<IPopupHost?>? _popupHostChangedHandler;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ToolTip"/> class. /// Initializes static members of the <see cref="ToolTip"/> class.
@ -251,35 +251,45 @@ namespace Avalonia.Controls
tooltip.RecalculatePosition(control); tooltip.RecalculatePosition(control);
} }
IPopupHost? IPopupHostProvider.PopupHost => _popupHost;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
internal void RecalculatePosition(Control control) 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) private void Open(Control control)
{ {
Close(); Close();
_popup = OverlayPopupHost.CreatePopupHost(control, null); _popupHost = OverlayPopupHost.CreatePopupHost(control, null);
_popup.SetChild(this); _popupHost.SetChild(this);
((ISetLogicalParent)_popup).SetParent(control); ((ISetLogicalParent)_popupHost).SetParent(control);
_popup.ConfigurePosition(control, GetPlacement(control), _popupHost.ConfigurePosition(control, GetPlacement(control),
new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
WindowManagerAddShadowHintChanged(_popup, false); WindowManagerAddShadowHintChanged(_popupHost, false);
_popup.Show(); _popupHost.Show();
_popupHostChangedHandler?.Invoke(_popupHost);
} }
private void Close() private void Close()
{ {
if (_popup != null) if (_popupHost != null)
{ {
_popup.SetChild(null); _popupHost.SetChild(null);
_popup.Dispose(); _popupHost.Dispose();
_popup = null; _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;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
@ -19,12 +20,11 @@ namespace Avalonia.Diagnostics.ViewModels
protected TreeNode(IVisual visual, TreeNode? parent) protected TreeNode(IVisual visual, TreeNode? parent)
{ {
_classes = string.Empty;
Parent = parent; Parent = parent;
Type = visual.GetType().Name; Type = visual.GetType().Name;
Visual = visual; Visual = visual;
_classes = string.Empty; FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal;
FontWeight = Visual is TopLevel || Visual is Popup ? FontWeight.Bold : FontWeight.Normal;
if (visual is IControl control) 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 FontWeight FontWeight { get; }
public abstract TreeNodeCollection Children public abstract TreeNodeCollection Children

97
src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@ -1,5 +1,10 @@
using System; using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Security.Cryptography.X509Certificates;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -13,7 +18,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
Children = new VisualTreeNodeCollection(this, visual); Children = new VisualTreeNodeCollection(this, visual);
if ((Visual is IStyleable styleable)) if (Visual is IStyleable styleable)
{ {
IsInTemplate = styleable.TemplatedParent != null; IsInTemplate = styleable.TemplatedParent != null;
} }
@ -25,13 +30,15 @@ namespace Avalonia.Diagnostics.ViewModels
public static VisualTreeNode[] Create(object control) 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 internal class VisualTreeNodeCollection : TreeNodeCollection
{ {
private readonly IVisual _control; private readonly IVisual _control;
private IDisposable? _subscription; private readonly CompositeDisposable _subscriptions = new CompositeDisposable(2);
public VisualTreeNodeCollection(TreeNode owner, IVisual control) public VisualTreeNodeCollection(TreeNode owner, IVisual control)
: base(owner) : base(owner)
@ -41,33 +48,79 @@ namespace Avalonia.Diagnostics.ViewModels
public override void Dispose() 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 => return Observable.FromEvent<IPopupHost?>(
{ x => popupHostProvider.PopupHostChanged += x,
if (child != null) x => popupHostProvider.PopupHostChanged -= x)
{ .StartWith(popupHostProvider.PopupHost)
nodes.Add(new VisualTreeNode(child, Owner)); .Select(x => x is IControl c ? c : null);
}
else
{
nodes.Clear();
}
});
} }
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.Insert(i, new VisualTreeNode(item, Owner)),
(i, item) => nodes.RemoveAt(i), (i, item) => nodes.RemoveAt(i),
() => nodes.Clear()); () => nodes.Clear()));
}
} }
} }
} }

Loading…
Cancel
Save