合并 Popup 相关代码

This commit is contained in:
polarboy 2024-07-23 16:45:57 +08:00
parent 3d6df89e0b
commit 9c633e52ab
11 changed files with 1470 additions and 1720 deletions

View File

@ -1,21 +0,0 @@
using AtomUI.Controls.Interceptors;
using AtomUI.Utils;
using Avalonia;
using HarmonyLib;
namespace AtomUI.Controls;
public class BootstrapInitializer : IBootstrapInitializer
{
public void Init()
{
AvaloniaLocator.CurrentMutable.BindToSelf(new ToolTipService());
InitInterceptors();
}
private void InitInterceptors()
{
var harmony = AvaloniaLocator.Current.GetService<Harmony>()!;
PopupInterceptorsRegister.Register(harmony);
}
}

View File

@ -2,7 +2,6 @@
using System.Reactive.Disposables;
using AtomUI.Controls.MotionScene;
using AtomUI.Controls.Utils;
using AtomUI.Data;
using AtomUI.MotionScene;
using AtomUI.Styling;
using AtomUI.Utils;
@ -130,7 +129,7 @@ public class Flyout : PopupFlyoutBase
private void HandlePopupPropertyChanged(AvaloniaPropertyChangedEventArgs args)
{
SetupArrowPosition(AtomPopup);
SetupArrowPosition(Popup);
}
private void SetupArrowPosition(Popup popup, FlyoutPresenter? flyoutPresenter = null)
@ -161,7 +160,7 @@ public class Flyout : PopupFlyoutBase
[!FlyoutPresenter.ChildProperty] = this[!ContentProperty]
};
BindUtils.RelayBind(this, IsShowArrowEffectiveProperty, presenter, IsShowArrowProperty);
SetupArrowPosition(AtomPopup, presenter);
SetupArrowPosition(Popup, presenter);
return presenter;
}
@ -405,10 +404,9 @@ public class Flyout : PopupFlyoutBase
private void PlayHideMotion()
{
var popup = AtomPopup;
var placementToplevel = TopLevel.GetTopLevel(popup.PlacementTarget);
var placementToplevel = TopLevel.GetTopLevel(Popup.PlacementTarget);
if (_popupPositionInfo is null ||
popup.Child is null ||
Popup.Child is null ||
placementToplevel is null) {
// 没有动画位置信息,直接关闭
base.HideCore(false);
@ -421,19 +419,19 @@ public class Flyout : PopupFlyoutBase
motion.ConfigureOpacity(_motionDuration);
motion.ConfigureRenderTransform(_motionDuration);
UIStructureUtils.SetVisualParent(popup.Child, null);
UIStructureUtils.SetVisualParent(popup.Child, null);
UIStructureUtils.SetVisualParent(Popup.Child, null);
UIStructureUtils.SetVisualParent(Popup.Child, null);
var motionActor = new PopupMotionActor(MaskShadows, _popupPositionInfo.Offset, _popupPositionInfo.Scaling,
popup.Child, motion);
Popup.Child, motion);
motionActor.DispatchInSceneLayer = true;
motionActor.SceneParent = placementToplevel;
motionActor.SceneShowed += (sender, args) =>
{
if (popup.Host is WindowBase window) {
if (Popup.Host is WindowBase window) {
window.Opacity = 0;
popup.HideShadowLayer();
Popup.HideShadowLayer();
}
};

View File

@ -1,17 +1,22 @@
using AtomUI.Utils;
using System.ComponentModel;
using AtomUI.Controls.Utils;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Logging;
namespace AtomUI.Controls;
using AvaloniaPopupFlyoutBase = Avalonia.Controls.Primitives.PopupFlyoutBase;
using PopupControl = Popup;
/// <summary>
/// 最基本得弹窗 Flyout在这里不处理那种带箭头得
/// </summary>
public abstract class PopupFlyoutBase : AvaloniaPopupFlyoutBase
public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
{
/// <summary>
/// 距离 anchor 的边距,根据垂直和水平进行设置
@ -19,14 +24,471 @@ public abstract class PopupFlyoutBase : AvaloniaPopupFlyoutBase
/// 还有些 anchor 和 gravity 的组合也没有用
/// </summary>
public static readonly StyledProperty<double> MarginToAnchorProperty =
PopupControl.MarginToAnchorProperty.AddOwner<PopupFlyoutBase>();
Popup.MarginToAnchorProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.PlacementProperty"/>
public static readonly StyledProperty<PlacementMode> PlacementProperty =
Popup.PlacementProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.HorizontalOffsetProperty"/>
public static readonly StyledProperty<double> HorizontalOffsetProperty =
Popup.HorizontalOffsetProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.VerticalOffsetProperty"/>
public static readonly StyledProperty<double> VerticalOffsetProperty =
Popup.VerticalOffsetProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
Popup.PlacementAnchorProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
Popup.PlacementGravityProperty.AddOwner<PopupFlyoutBase>();
/// <summary>
/// Defines the <see cref="ShowMode"/> property
/// </summary>
public static readonly StyledProperty<FlyoutShowMode> ShowModeProperty =
AvaloniaProperty.Register<PopupFlyoutBase, FlyoutShowMode>(nameof(ShowMode));
/// <summary>
/// Defines the <see cref="OverlayInputPassThroughElement"/> property
/// </summary>
public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
Popup.OverlayInputPassThroughElementProperty.AddOwner<PopupFlyoutBase>();
/// <summary>
/// Defines the <see cref="PlacementConstraintAdjustment"/> property
/// </summary>
public static readonly StyledProperty<PopupPositionerConstraintAdjustment> PlacementConstraintAdjustmentProperty =
Popup.PlacementConstraintAdjustmentProperty.AddOwner<PopupFlyoutBase>();
protected Popup Popup => _popupLazy.Value;
/// <inheritdoc cref="Popup.Placement"/>
public PlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <inheritdoc cref="Popup.PlacementGravity"/>
public PopupGravity PlacementGravity
{
get => GetValue(PlacementGravityProperty);
set => SetValue(PlacementGravityProperty, value);
}
/// <inheritdoc cref="Popup.PlacementAnchor"/>
public PopupAnchor PlacementAnchor
{
get => GetValue(PlacementAnchorProperty);
set => SetValue(PlacementAnchorProperty, value);
}
/// <inheritdoc cref="Popup.HorizontalOffset"/>
public double HorizontalOffset
{
get => GetValue(HorizontalOffsetProperty);
set => SetValue(HorizontalOffsetProperty, value);
}
/// <inheritdoc cref="Popup.VerticalOffset"/>
public double VerticalOffset
{
get => GetValue(VerticalOffsetProperty);
set => SetValue(VerticalOffsetProperty, value);
}
/// <summary>
/// Gets or sets the desired ShowMode
/// </summary>
public FlyoutShowMode ShowMode
{
get => GetValue(ShowModeProperty);
set => SetValue(ShowModeProperty, value);
}
/// <summary>
/// Gets or sets an element that should receive pointer input events even when underneath
/// the flyout's overlay.
/// </summary>
public IInputElement? OverlayInputPassThroughElement
{
get => GetValue(OverlayInputPassThroughElementProperty);
set => SetValue(OverlayInputPassThroughElementProperty, value);
}
/// <inheritdoc cref="Popup.PlacementConstraintAdjustment"/>
public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
{
get => GetValue(PlacementConstraintAdjustmentProperty);
set => SetValue(PlacementConstraintAdjustmentProperty, value);
}
IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host;
private readonly Lazy<Popup> _popupLazy;
private Rect? _enlargedPopupRect;
private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable;
private Action<IPopupHost?>? _popupHostChangedHandler;
public double MarginToAnchor
{
get => GetValue(MarginToAnchorProperty);
set => SetValue(MarginToAnchorProperty, value);
}
static PopupFlyoutBase()
{
Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
}
public PopupFlyoutBase()
{
_popupLazy = new Lazy<Popup>(() => CreatePopup());
}
protected internal virtual void NotifyPopupCreated(Popup popup)
{
BindUtils.RelayBind(this, MarginToAnchorProperty, popup);
}
protected internal virtual void NotifyPositionPopup(bool showAtPointer)
{
Size sz;
// Popup.Child can't be null here, it was set in ShowAtCore.
if (Popup.Child!.DesiredSize == default) {
// Popup may not have been shown yet. Measure content
sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
} else {
sz = Popup.Child.DesiredSize;
}
Popup.VerticalOffset = VerticalOffset;
Popup.HorizontalOffset = HorizontalOffset;
Popup.PlacementAnchor = PlacementAnchor;
Popup.PlacementGravity = PlacementGravity;
if (showAtPointer) {
Popup.Placement = PlacementMode.Pointer;
} else {
Popup.Placement = Placement;
Popup.PlacementConstraintAdjustment = PlacementConstraintAdjustment;
}
}
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
public event EventHandler<CancelEventArgs>? Closing;
public event EventHandler? Opening;
/// <summary>
/// Shows the Flyout at the given Control
/// </summary>
/// <param name="placementTarget">The control to show the Flyout at</param>
public sealed override void ShowAt(Control placementTarget)
{
ShowAtCore(placementTarget);
}
/// <summary>
/// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout
/// </summary>
/// <param name="placementTarget">The target control</param>
/// <param name="showAtPointer">True to show at pointer</param>
public void ShowAt(Control placementTarget, bool showAtPointer)
{
ShowAtCore(placementTarget, showAtPointer);
}
/// <summary>
/// Hides the Flyout
/// </summary>
public sealed override void Hide()
{
HideCore();
}
/// <returns>True, if action was handled</returns>
protected virtual bool HideCore(bool canCancel = true)
{
if (!IsOpen) {
return false;
}
if (canCancel) {
if (CancelClosing()) {
return false;
}
}
IsOpen = false;
Popup.IsOpen = false;
((ISetLogicalParent)Popup).SetParent(null);
// Ensure this isn't active
_transientDisposable?.Dispose();
_transientDisposable = null;
_enlargedPopupRect = null;
_enlargePopupRectScreenPixelRect = null;
if (Target != null) {
Target.DetachedFromVisualTree -= PlacementTarget_DetachedFromVisualTree;
Target.KeyUp -= OnPlacementTargetOrPopupKeyUp;
}
OnClosed();
Target = null;
return true;
}
/// <returns>True, if action was handled</returns>
protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = false)
{
if (placementTarget == null) {
throw new ArgumentNullException(nameof(placementTarget));
}
if (IsOpen) {
if (placementTarget == Target) {
return false;
} else // Close before opening a new one
{
_ = HideCore(false);
}
}
if (Popup.Parent != null && Popup.Parent != placementTarget) {
((ISetLogicalParent)Popup).SetParent(null);
}
if (Popup.Parent == null || Popup.PlacementTarget != placementTarget) {
Popup.PlacementTarget = Target = placementTarget;
((ISetLogicalParent)Popup).SetParent(placementTarget);
UIStructureUtils.SetTemplateParent(Popup, placementTarget.TemplatedParent);
}
if (Popup.Child == null) {
Popup.Child = CreatePresenter();
}
Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement;
if (CancelOpening()) {
return false;
}
NotifyPositionPopup(showAtPointer);
IsOpen = Popup.IsOpen = true;
OnOpened();
placementTarget.DetachedFromVisualTree += PlacementTarget_DetachedFromVisualTree;
placementTarget.KeyUp += OnPlacementTargetOrPopupKeyUp;
if (ShowMode == FlyoutShowMode.Standard) {
// Try and focus content inside Flyout
if (Popup.Child.Focusable) {
Popup.Child.Focus();
} else {
var nextFocus = KeyboardNavigationHandler.GetNext(Popup.Child, NavigationDirection.Next);
nextFocus?.Focus();
}
} else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway) {
var inputManager = AvaloniaLocator.Current.GetService<IInputManager>();
_transientDisposable = inputManager?.Process.Subscribe(HandleTransientDismiss);
}
return true;
}
private void PlacementTarget_DetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_ = HideCore(false);
}
private void HandleTransientDismiss(RawInputEventArgs args)
{
if (args is RawPointerEventArgs pArgs && pArgs.Type == RawPointerEventType.Move) {
// In ShowMode = TransientWithDismissOnPointerMoveAway, the Flyout is kept
// shown as long as the pointer is within a certain px distance from the
// flyout itself. I'm not sure what WinUI uses, but I'm defaulting to
// 100px, which seems about right
// enlargedPopupRect is the Flyout bounds enlarged 100px
// For windowed popups, enlargedPopupRect is in screen coordinates,
// for overlay popups, its in OverlayLayer coordinates
if (_enlargedPopupRect == null && _enlargePopupRectScreenPixelRect == null) {
// Only do this once when the Flyout opens & cache the result
if (Popup?.Host is PopupRoot root) {
// Get the popup root bounds and convert to screen coordinates
var tmp = root.Bounds.Inflate(100);
_enlargePopupRectScreenPixelRect =
new PixelRect(root.PointToScreen(tmp.TopLeft), root.PointToScreen(tmp.BottomRight));
} else if (Popup?.Host is OverlayPopupHost host) {
// Overlay popups are in OverlayLayer coordinates, just use that
_enlargedPopupRect = host.Bounds.Inflate(100);
}
return;
}
if (Popup?.Host is PopupRoot && pArgs.Root is Visual eventRoot) {
// As long as the pointer stays within the enlargedPopupRect
// the flyout stays open. If it leaves, close it
// Despite working in screen coordinates, leaving the TopLevel
// window will not close this (as pointer events stop), which
// does match UWP
var pt = eventRoot.PointToScreen(pArgs.Position);
if (!_enlargePopupRectScreenPixelRect?.Contains(pt) ?? false) {
HideCore(false);
}
} else if (Popup?.Host is OverlayPopupHost) {
// Same as above here, but just different coordinate space
// so we don't need to translate
if (!_enlargedPopupRect?.Contains(pArgs.Position) ?? false) {
HideCore(false);
}
}
}
}
protected virtual void OnOpening(CancelEventArgs args)
{
Opening?.Invoke(this, args);
}
protected virtual void OnClosing(CancelEventArgs args)
{
Closing?.Invoke(this, args);
}
/// <summary>
/// Used to create the content the Flyout displays
/// </summary>
/// <returns></returns>
protected abstract Control CreatePresenter();
private Popup CreatePopup()
{
var popup = new Popup
{
WindowManagerAddShadowHint = false,
IsLightDismissEnabled = false,
//Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss.
OverlayDismissEventPassThrough = false
};
popup.Opened += OnPopupOpened;
popup.Closed += OnPopupClosed;
popup.Closing += OnPopupClosing;
popup.KeyUp += OnPlacementTargetOrPopupKeyUp;
NotifyPopupCreated(popup);
return popup;
}
private void OnPopupOpened(object? sender, EventArgs e)
{
IsOpen = true;
_popupHostChangedHandler?.Invoke(Popup.Host);
}
private void OnPopupClosing(object? sender, CancelEventArgs e)
{
if (IsOpen) {
e.Cancel = CancelClosing();
if (!e.Cancel) {
if (Popup.Host is not null) {
Popup.NotifyAboutToClosing();
}
}
}
}
private void OnPopupClosed(object? sender, EventArgs e)
{
HideCore(false);
_popupHostChangedHandler?.Invoke(null);
}
// This method is handling both popup logical tree and target logical tree.
private void OnPlacementTargetOrPopupKeyUp(object? sender, KeyEventArgs e)
{
if (!e.Handled
&& IsOpen
&& Target?.ContextFlyout == this) {
var keymap = Application.Current!.PlatformSettings?.HotkeyConfiguration;
if (keymap?.OpenContextMenu.Any(k => k.Matches(e)) == true) {
e.Handled = HideCore();
}
}
}
private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args)
{
if (args.Sender is Control c) {
if (args.OldValue is FlyoutBase) {
c.ContextRequested -= OnControlContextRequested;
}
if (args.NewValue is FlyoutBase) {
c.ContextRequested += OnControlContextRequested;
}
}
}
private static void OnControlContextRequested(object? sender, ContextRequestedEventArgs e)
{
if (!e.Handled
&& sender is Control control
&& control.ContextFlyout is { } flyout) {
if (control.ContextMenu != null) {
Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")
?.Log(control, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
return;
}
if (flyout is PopupFlyoutBase popupFlyout) {
// We do not support absolute popup positioning yet, so we ignore "point" at this moment.
var triggeredByPointerInput = e.TryGetPosition(null, out _);
e.Handled = popupFlyout.ShowAtCore(control, triggeredByPointerInput);
} else {
flyout.ShowAt(control);
e.Handled = true;
}
}
}
private bool CancelClosing()
{
var eventArgs = new CancelEventArgs();
OnClosing(eventArgs);
return eventArgs.Cancel;
}
private bool CancelOpening()
{
var eventArgs = new CancelEventArgs();
OnOpening(eventArgs);
return eventArgs.Cancel;
}
internal static void SetPresenterClasses(Control? presenter, Classes classes)
{
if (presenter is null) {
@ -44,36 +506,4 @@ public abstract class PopupFlyoutBase : AvaloniaPopupFlyoutBase
//Add new classes
presenter.Classes.AddRange(classes);
}
protected internal virtual void NotifyPopupCreated(Popup popup)
{
BindUtils.RelayBind(this, MarginToAnchorProperty, popup);
}
protected internal virtual void NotifyPositionPopup(bool showAtPointer)
{
Size sz;
// Popup.Child can't be null here, it was set in ShowAtCore.
if (Popup.Child!.DesiredSize == default) {
// Popup may not have been shown yet. Measure content
sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
} else {
sz = Popup.Child.DesiredSize;
}
Popup.VerticalOffset = VerticalOffset;
Popup.HorizontalOffset = HorizontalOffset;
Popup.PlacementAnchor = PlacementAnchor;
Popup.PlacementGravity = PlacementGravity;
if (showAtPointer) {
Popup.Placement = PlacementMode.Pointer;
} else {
Popup.Placement = Placement;
Popup.PlacementConstraintAdjustment = PlacementConstraintAdjustment;
}
}
protected Popup AtomPopup => (Popup as Popup)!;
}

View File

@ -1,520 +0,0 @@
using System.ComponentModel;
using AtomUI.Controls.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Logging;
namespace AtomUI.Controls;
public abstract class PopupFlyoutBaseX : FlyoutBase, IPopupHostProvider
{
/// <inheritdoc cref="Popup.PlacementProperty"/>
public static readonly StyledProperty<PlacementMode> PlacementProperty =
Popup.PlacementProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.HorizontalOffsetProperty"/>
public static readonly StyledProperty<double> HorizontalOffsetProperty =
Popup.HorizontalOffsetProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.VerticalOffsetProperty"/>
public static readonly StyledProperty<double> VerticalOffsetProperty =
Popup.VerticalOffsetProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
Popup.PlacementAnchorProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
Popup.PlacementGravityProperty.AddOwner<PopupFlyoutBase>();
/// <summary>
/// Defines the <see cref="ShowMode"/> property
/// </summary>
public static readonly StyledProperty<FlyoutShowMode> ShowModeProperty =
AvaloniaProperty.Register<PopupFlyoutBase, FlyoutShowMode>(nameof(ShowMode));
/// <summary>
/// Defines the <see cref="OverlayInputPassThroughElement"/> property
/// </summary>
public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
Popup.OverlayInputPassThroughElementProperty.AddOwner<PopupFlyoutBase>();
/// <summary>
/// Defines the <see cref="PlacementConstraintAdjustment"/> property
/// </summary>
public static readonly StyledProperty<PopupPositionerConstraintAdjustment> PlacementConstraintAdjustmentProperty =
Popup.PlacementConstraintAdjustmentProperty.AddOwner<PopupFlyoutBase>();
private readonly Lazy<Popup> _popupLazy;
private Rect? _enlargedPopupRect;
private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable;
private Action<IPopupHost?>? _popupHostChangedHandler;
static PopupFlyoutBaseX()
{
Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
}
public PopupFlyoutBaseX()
{
_popupLazy = new Lazy<Popup>(() => CreatePopup());
}
protected Popup Popup => _popupLazy.Value;
/// <inheritdoc cref="Popup.Placement"/>
public PlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <inheritdoc cref="Popup.PlacementGravity"/>
public PopupGravity PlacementGravity
{
get => GetValue(PlacementGravityProperty);
set => SetValue(PlacementGravityProperty, value);
}
/// <inheritdoc cref="Popup.PlacementAnchor"/>
public PopupAnchor PlacementAnchor
{
get => GetValue(PlacementAnchorProperty);
set => SetValue(PlacementAnchorProperty, value);
}
/// <inheritdoc cref="Popup.HorizontalOffset"/>
public double HorizontalOffset
{
get => GetValue(HorizontalOffsetProperty);
set => SetValue(HorizontalOffsetProperty, value);
}
/// <inheritdoc cref="Popup.VerticalOffset"/>
public double VerticalOffset
{
get => GetValue(VerticalOffsetProperty);
set => SetValue(VerticalOffsetProperty, value);
}
/// <summary>
/// Gets or sets the desired ShowMode
/// </summary>
public FlyoutShowMode ShowMode
{
get => GetValue(ShowModeProperty);
set => SetValue(ShowModeProperty, value);
}
/// <summary>
/// Gets or sets an element that should receive pointer input events even when underneath
/// the flyout's overlay.
/// </summary>
public IInputElement? OverlayInputPassThroughElement
{
get => GetValue(OverlayInputPassThroughElementProperty);
set => SetValue(OverlayInputPassThroughElementProperty, value);
}
/// <inheritdoc cref="Popup.PlacementConstraintAdjustment"/>
public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
{
get => GetValue(PlacementConstraintAdjustmentProperty);
set => SetValue(PlacementConstraintAdjustmentProperty, value);
}
IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
public event EventHandler<CancelEventArgs>? Closing;
public event EventHandler? Opening;
/// <summary>
/// Shows the Flyout at the given Control
/// </summary>
/// <param name="placementTarget">The control to show the Flyout at</param>
public sealed override void ShowAt(Control placementTarget)
{
ShowAtCore(placementTarget);
}
/// <summary>
/// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout
/// </summary>
/// <param name="placementTarget">The target control</param>
/// <param name="showAtPointer">True to show at pointer</param>
public void ShowAt(Control placementTarget, bool showAtPointer)
{
ShowAtCore(placementTarget, showAtPointer);
}
/// <summary>
/// Hides the Flyout
/// </summary>
public sealed override void Hide()
{
HideCore();
}
/// <returns>True, if action was handled</returns>
protected virtual bool HideCore(bool canCancel = true)
{
if (!IsOpen)
{
return false;
}
if (canCancel)
{
if (CancelClosing())
{
return false;
}
}
IsOpen = false;
Popup.IsOpen = false;
((ISetLogicalParent)Popup).SetParent(null);
// Ensure this isn't active
_transientDisposable?.Dispose();
_transientDisposable = null;
_enlargedPopupRect = null;
_enlargePopupRectScreenPixelRect = null;
if (Target != null)
{
Target.DetachedFromVisualTree -= PlacementTarget_DetachedFromVisualTree;
Target.KeyUp -= OnPlacementTargetOrPopupKeyUp;
}
OnClosed();
Target = null;
return true;
}
/// <returns>True, if action was handled</returns>
protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = false)
{
if (placementTarget == null)
{
throw new ArgumentNullException(nameof(placementTarget));
}
if (IsOpen)
{
if (placementTarget == Target)
{
return false;
}
else // Close before opening a new one
{
_ = HideCore(false);
}
}
if (Popup.Parent != null && Popup.Parent != placementTarget)
{
((ISetLogicalParent)Popup).SetParent(null);
}
if (Popup.Parent == null || Popup.PlacementTarget != placementTarget)
{
Popup.PlacementTarget = Target = placementTarget;
((ISetLogicalParent)Popup).SetParent(placementTarget);
UIStructureUtils.SetTemplateParent(Popup, placementTarget.TemplatedParent);
}
if (Popup.Child == null)
{
Popup.Child = CreatePresenter();
}
Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement;
if (CancelOpening())
{
return false;
}
PositionPopup(showAtPointer);
IsOpen = Popup.IsOpen = true;
OnOpened();
placementTarget.DetachedFromVisualTree += PlacementTarget_DetachedFromVisualTree;
placementTarget.KeyUp += OnPlacementTargetOrPopupKeyUp;
if (ShowMode == FlyoutShowMode.Standard)
{
// Try and focus content inside Flyout
if (Popup.Child.Focusable)
{
Popup.Child.Focus();
}
else
{
var nextFocus = KeyboardNavigationHandler.GetNext(Popup.Child, NavigationDirection.Next);
nextFocus?.Focus();
}
}
else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway)
{
var inputManager = AvaloniaLocator.Current.GetService<IInputManager>();
_transientDisposable = inputManager?.Process.Subscribe(HandleTransientDismiss);
}
return true;
}
private void PlacementTarget_DetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_ = HideCore(false);
}
private void HandleTransientDismiss(RawInputEventArgs args)
{
if (args is RawPointerEventArgs pArgs && pArgs.Type == RawPointerEventType.Move)
{
// In ShowMode = TransientWithDismissOnPointerMoveAway, the Flyout is kept
// shown as long as the pointer is within a certain px distance from the
// flyout itself. I'm not sure what WinUI uses, but I'm defaulting to
// 100px, which seems about right
// enlargedPopupRect is the Flyout bounds enlarged 100px
// For windowed popups, enlargedPopupRect is in screen coordinates,
// for overlay popups, its in OverlayLayer coordinates
if (_enlargedPopupRect == null && _enlargePopupRectScreenPixelRect == null)
{
// Only do this once when the Flyout opens & cache the result
if (Popup?.Host is PopupRoot root)
{
// Get the popup root bounds and convert to screen coordinates
var tmp = root.Bounds.Inflate(100);
_enlargePopupRectScreenPixelRect = new PixelRect(root.PointToScreen(tmp.TopLeft), root.PointToScreen(tmp.BottomRight));
}
else if (Popup?.Host is OverlayPopupHost host)
{
// Overlay popups are in OverlayLayer coordinates, just use that
_enlargedPopupRect = host.Bounds.Inflate(100);
}
return;
}
if (Popup?.Host is PopupRoot && pArgs.Root is Visual eventRoot)
{
// As long as the pointer stays within the enlargedPopupRect
// the flyout stays open. If it leaves, close it
// Despite working in screen coordinates, leaving the TopLevel
// window will not close this (as pointer events stop), which
// does match UWP
var pt = eventRoot.PointToScreen(pArgs.Position);
if (!_enlargePopupRectScreenPixelRect?.Contains(pt) ?? false)
{
HideCore(false);
}
}
else if (Popup?.Host is OverlayPopupHost)
{
// Same as above here, but just different coordinate space
// so we don't need to translate
if (!_enlargedPopupRect?.Contains(pArgs.Position) ?? false)
{
HideCore(false);
}
}
}
}
protected virtual void OnOpening(CancelEventArgs args)
{
Opening?.Invoke(this, args);
}
protected virtual void OnClosing(CancelEventArgs args)
{
Closing?.Invoke(this, args);
}
/// <summary>
/// Used to create the content the Flyout displays
/// </summary>
/// <returns></returns>
protected abstract Control CreatePresenter();
private Popup CreatePopup()
{
var popup = new Popup
{
WindowManagerAddShadowHint = false,
IsLightDismissEnabled = true,
//Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss.
OverlayDismissEventPassThrough = false
};
popup.Opened += OnPopupOpened;
popup.Closed += OnPopupClosed;
//popup.Closing += OnPopupClosing;
popup.KeyUp += OnPlacementTargetOrPopupKeyUp;
return popup;
}
private void OnPopupOpened(object? sender, EventArgs e)
{
IsOpen = true;
_popupHostChangedHandler?.Invoke(Popup.Host);
}
private void OnPopupClosing(object? sender, CancelEventArgs e)
{
if (IsOpen)
{
e.Cancel = CancelClosing();
}
}
private void OnPopupClosed(object? sender, EventArgs e)
{
HideCore(false);
_popupHostChangedHandler?.Invoke(null);
}
// This method is handling both popup logical tree and target logical tree.
private void OnPlacementTargetOrPopupKeyUp(object? sender, KeyEventArgs e)
{
if (!e.Handled
&& IsOpen
&& Target?.ContextFlyout == this)
{
var keymap = Application.Current!.PlatformSettings?.HotkeyConfiguration;
if (keymap?.OpenContextMenu.Any(k => k.Matches(e)) == true)
{
e.Handled = HideCore();
}
}
}
private void PositionPopup(bool showAtPointer)
{
Size sz;
// Popup.Child can't be null here, it was set in ShowAtCore.
if (Popup.Child!.DesiredSize == default)
{
// Popup may not have been shown yet. Measure content
sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
}
else
{
sz = Popup.Child.DesiredSize;
}
Popup.VerticalOffset = VerticalOffset;
Popup.HorizontalOffset = HorizontalOffset;
Popup.PlacementAnchor = PlacementAnchor;
Popup.PlacementGravity = PlacementGravity;
if (showAtPointer)
{
Popup.Placement = PlacementMode.Pointer;
}
else
{
Popup.Placement = Placement;
Popup.PlacementConstraintAdjustment = PlacementConstraintAdjustment;
}
}
private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args)
{
if (args.Sender is Control c)
{
if (args.OldValue is FlyoutBase)
{
c.ContextRequested -= OnControlContextRequested;
}
if (args.NewValue is FlyoutBase)
{
c.ContextRequested += OnControlContextRequested;
}
}
}
private static void OnControlContextRequested(object? sender, ContextRequestedEventArgs e)
{
if (!e.Handled
&& sender is Control control
&& control.ContextFlyout is { } flyout)
{
if (control.ContextMenu != null)
{
Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(control, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
return;
}
if (flyout is PopupFlyoutBaseX popupFlyout)
{
// We do not support absolute popup positioning yet, so we ignore "point" at this moment.
var triggeredByPointerInput = e.TryGetPosition(null, out _);
e.Handled = popupFlyout.ShowAtCore(control, triggeredByPointerInput);
}
else
{
flyout.ShowAt(control);
e.Handled = true;
}
}
}
private bool CancelClosing()
{
var eventArgs = new CancelEventArgs();
OnClosing(eventArgs);
return eventArgs.Cancel;
}
private bool CancelOpening()
{
var eventArgs = new CancelEventArgs();
OnOpening(eventArgs);
return eventArgs.Cancel;
}
internal static void SetPresenterClasses(Control? presenter, Classes classes)
{
if(presenter is null)
{
return;
}
//Remove any classes no longer in use, ignoring pseudo classes
for (int i = presenter.Classes.Count - 1; i >= 0; i--)
{
if (!classes.Contains(presenter.Classes[i]) &&
!presenter.Classes[i].Contains(':'))
{
presenter.Classes.RemoveAt(i);
}
}
//Add new classes
presenter.Classes.AddRange(classes);
}
}

View File

@ -1,166 +0,0 @@
using System.ComponentModel;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using HarmonyLib;
namespace AtomUI.Controls.Interceptors;
using AvaloniaPopupFlyoutBase = Avalonia.Controls.Primitives.PopupFlyoutBase;
using AvaloniaPopup = Avalonia.Controls.Primitives.Popup;
internal static class PopupFlyoutBaseInterceptor
{
private static readonly EventInfo OpenedEventInfo;
private static readonly EventInfo ClosingEventInfo;
private static readonly EventInfo ClosedEventInfo;
private static readonly EventInfo KeyUpEventInfo;
private static readonly MethodInfo OnPopupOpenedMemberInfo;
private static readonly MethodInfo OnPopupClosedMemberInfo;
private static readonly MethodInfo OnPopupClosingMethodInfo;
private static readonly MethodInfo OnPlacementTargetOrPopupKeyUpMethodInfo;
static PopupFlyoutBaseInterceptor()
{
var popupType = typeof(AvaloniaPopup);
var popupFlyoutBaseType = typeof(AvaloniaPopupFlyoutBase);
OpenedEventInfo = popupType.GetEvent("Opened")!;
ClosingEventInfo = popupType.GetEvent("Closing", BindingFlags.NonPublic | BindingFlags.Instance)!;
ClosedEventInfo = popupType.GetEvent("Closed")!;
KeyUpEventInfo = popupType.GetEvent("KeyUp")!;
OnPopupOpenedMemberInfo = popupFlyoutBaseType.GetMethod("OnPopupOpened", BindingFlags.Instance | BindingFlags.NonPublic)!;
OnPopupClosingMethodInfo = popupFlyoutBaseType.GetMethod("OnPopupClosing", BindingFlags.Instance | BindingFlags.NonPublic)!;
OnPopupClosedMemberInfo = popupFlyoutBaseType.GetMethod("OnPopupClosed", BindingFlags.Instance | BindingFlags.NonPublic)!;
OnPlacementTargetOrPopupKeyUpMethodInfo = popupFlyoutBaseType.GetMethod("OnPlacementTargetOrPopupKeyUp", BindingFlags.Instance | BindingFlags.NonPublic)!;
}
public static bool CreatePopupPrefixInterceptor(PopupFlyoutBase __instance, ref AvaloniaPopup __result)
{
if (typeof(PopupFlyoutBase).IsAssignableFrom(__instance.GetType())) {
var popup = new Popup
{
WindowManagerAddShadowHint = false,
IsLightDismissEnabled = false,
};
OpenedEventInfo.AddEventHandler(popup, Delegate.CreateDelegate(typeof(EventHandler), __instance, OnPopupOpenedMemberInfo));
EventHandler<CancelEventArgs> closingDelegate = (object? sender, CancelEventArgs e) =>
{
OnPopupClosingMethodInfo.Invoke(__instance, new object?[] { sender, e });
if (!e.Cancel) {
if (popup._popupHost is not null) {
popup.NotifyAboutToClosing();
}
}
};
ClosingEventInfo.AddMethod!.Invoke(popup, new object?[]
{
closingDelegate
});
ClosedEventInfo.AddEventHandler(popup, Delegate.CreateDelegate(typeof(EventHandler<EventArgs>), __instance, OnPopupClosedMemberInfo));
KeyUpEventInfo.AddEventHandler(popup, Delegate.CreateDelegate(typeof(EventHandler<KeyEventArgs>), __instance, OnPlacementTargetOrPopupKeyUpMethodInfo));
__result = popup;
__instance.NotifyPopupCreated(popup);
return false;
}
return true;
}
public static void UpdateHostPositionPostfixInterceptor(AbstractPopup __instance, IPopupHost popupHost, Control placementTarget)
{
if (__instance is Popup) {
__instance.NotifyHostPositionUpdated(popupHost, placementTarget);
}
}
public static bool UpdateHostPositionPrefixInterceptor(AbstractPopup __instance, IPopupHost popupHost, Control placementTarget)
{
if (typeof(AbstractPopup).IsAssignableFrom(__instance.GetType())) {
__instance.NotifyAboutToUpdateHostPosition(popupHost, placementTarget);
}
return true;
}
public static bool PositionPopupInterceptor(PopupFlyoutBase __instance, bool showAtPointer)
{
if (typeof(PopupFlyoutBase).IsAssignableFrom(__instance.GetType())) {
__instance.NotifyPositionPopup(showAtPointer);
return false;
}
return true;
}
}
internal static class PopupRootInterceptor
{
private static readonly Type PopupRootType = typeof(PopupRoot);
public static bool ShowPrefixInterceptor(PopupRoot __instance)
{
// TODO 这个范围有点广,需要评估
if (PopupRootType.IsInstanceOfType(__instance)) {
if (__instance.Parent is Popup popup) {
popup.NotifyPopupRootAboutToShow(__instance);
}
}
return true;
}
}
internal static class PopupInterceptorsRegister
{
public static void Register(Harmony harmony)
{
// RegisterPopupFlyoutBaseCreatePopup(harmony);
// RegisterPopupUpdateHostPositionPrefix(harmony);
// RegisterPopupUpdateHostPositionPostfix(harmony);
// RegisterPopupPositionPopup(harmony);
// RegisterPopupRootShow(harmony);
}
private static void RegisterPopupFlyoutBaseCreatePopup(Harmony harmony)
{
var origin = typeof(AvaloniaPopupFlyoutBase).GetMethod("CreatePopup", BindingFlags.Instance | BindingFlags.NonPublic);
var prefixInterceptor = typeof(PopupFlyoutBaseInterceptor)
.GetMethod(nameof(PopupFlyoutBaseInterceptor.CreatePopupPrefixInterceptor),
BindingFlags.Static | BindingFlags.Public);
harmony.Patch(origin, prefix: new HarmonyMethod(prefixInterceptor));
}
private static void RegisterPopupUpdateHostPositionPrefix(Harmony harmony)
{
var origin = typeof(AvaloniaPopup).GetMethod("UpdateHostPosition", BindingFlags.Instance | BindingFlags.NonPublic);
var prefixInterceptor = typeof(PopupFlyoutBaseInterceptor)
.GetMethod(nameof(PopupFlyoutBaseInterceptor.UpdateHostPositionPrefixInterceptor),
BindingFlags.Static | BindingFlags.Public);
harmony.Patch(origin, prefix: new HarmonyMethod(prefixInterceptor));
}
private static void RegisterPopupUpdateHostPositionPostfix(Harmony harmony)
{
var origin = typeof(AvaloniaPopup).GetMethod("UpdateHostPosition", BindingFlags.Instance | BindingFlags.NonPublic);
var postfixInterceptor = typeof(PopupFlyoutBaseInterceptor)
.GetMethod(nameof(PopupFlyoutBaseInterceptor.UpdateHostPositionPostfixInterceptor),
BindingFlags.Static | BindingFlags.Public);
harmony.Patch(origin, postfix: new HarmonyMethod(postfixInterceptor));
}
private static void RegisterPopupPositionPopup(Harmony harmony)
{
var origin = typeof(AvaloniaPopupFlyoutBase).GetMethod("PositionPopup", BindingFlags.Instance | BindingFlags.NonPublic);
var prefixInterceptor = typeof(PopupFlyoutBaseInterceptor)
.GetMethod(nameof(PopupFlyoutBaseInterceptor.PositionPopupInterceptor),
BindingFlags.Static | BindingFlags.Public);
harmony.Patch(origin, prefix: new HarmonyMethod(prefixInterceptor));
}
private static void RegisterPopupRootShow(Harmony harmony)
{
var origin = typeof(WindowBase).GetMethod("Show", BindingFlags.Instance | BindingFlags.Public);
var prefixInterceptor = typeof(PopupRootInterceptor)
.GetMethod(nameof(PopupRootInterceptor.ShowPrefixInterceptor),
BindingFlags.Static | BindingFlags.Public);
harmony.Patch(origin, prefix: new HarmonyMethod(prefixInterceptor));
}
}

View File

@ -1,47 +1,358 @@
using System.Reflection;
using System.ComponentModel;
using System.Reactive.Disposables;
using System.Reflection;
using AtomUI.Controls.Utils;
using AtomUI.Input;
using AtomUI.Reactive;
using Avalonia;
using Avalonia.Automation.Peers;
using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace AtomUI.Controls;
using AvaloniaPopup = Avalonia.Controls.Primitives.Popup;
public abstract class AbstractPopup : AvaloniaPopup
public abstract class AbstractPopup : Control, IPopupHostProvider
{
public static readonly StyledProperty<bool> WindowManagerAddShadowHintProperty =
AvaloniaProperty.Register<AbstractPopup, bool>(nameof(WindowManagerAddShadowHint), false);
/// <summary>
/// Defines the <see cref="Child"/> property.
/// </summary>
public static readonly StyledProperty<Control?> ChildProperty =
AvaloniaProperty.Register<AbstractPopup, Control?>(nameof(Child));
/// <summary>
/// Defines the <see cref="InheritsTransform"/> property.
/// </summary>
public static readonly StyledProperty<bool> InheritsTransformProperty =
AvaloniaProperty.Register<AbstractPopup, bool>(nameof(InheritsTransform));
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsOpenProperty =
AvaloniaProperty.Register<AbstractPopup, bool>(nameof(IsOpen));
/// <summary>
/// Defines the <see cref="PlacementAnchor"/> property.
/// </summary>
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
AvaloniaProperty.Register<AbstractPopup, PopupAnchor>(nameof(PlacementAnchor));
/// <summary>
/// Defines the <see cref="PlacementConstraintAdjustment"/> property.
/// </summary>
public static readonly StyledProperty<PopupPositionerConstraintAdjustment> PlacementConstraintAdjustmentProperty =
AvaloniaProperty.Register<AbstractPopup, PopupPositionerConstraintAdjustment>(
nameof(PlacementConstraintAdjustment),
PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY |
PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY |
PopupPositionerConstraintAdjustment.ResizeX | PopupPositionerConstraintAdjustment.ResizeY);
/// <summary>
/// Defines the <see cref="PlacementGravity"/> property.
/// </summary>
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
AvaloniaProperty.Register<AbstractPopup, PopupGravity>(nameof(PlacementGravity));
/// <summary>
/// Defines the <see cref="Placement"/> property.
/// </summary>
public static readonly StyledProperty<PlacementMode> PlacementProperty =
AvaloniaProperty.Register<AbstractPopup, PlacementMode>(nameof(Placement), defaultValue: PlacementMode.Bottom);
/// <summary>
/// Defines the <see cref="PlacementMode"/> property.
/// </summary>
[Obsolete("Use the Placement property instead."), EditorBrowsable(EditorBrowsableState.Never)]
public static readonly StyledProperty<PlacementMode> PlacementModeProperty = PlacementProperty;
/// <summary>
/// Defines the <see cref="PlacementRect"/> property.
/// </summary>
public static readonly StyledProperty<Rect?> PlacementRectProperty =
AvaloniaProperty.Register<AbstractPopup, Rect?>(nameof(PlacementRect));
/// <summary>
/// Defines the <see cref="PlacementTarget"/> property.
/// </summary>
public static readonly StyledProperty<Control?> PlacementTargetProperty =
AvaloniaProperty.Register<AbstractPopup, Control?>(nameof(PlacementTarget));
public static readonly StyledProperty<bool> OverlayDismissEventPassThroughProperty =
AvaloniaProperty.Register<AbstractPopup, bool>(nameof(OverlayDismissEventPassThrough));
public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
AvaloniaProperty.Register<AbstractPopup, IInputElement?>(nameof(OverlayInputPassThroughElement));
/// <summary>
/// Defines the <see cref="HorizontalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> HorizontalOffsetProperty =
AvaloniaProperty.Register<AbstractPopup, double>(nameof(HorizontalOffset));
/// <summary>
/// Defines the <see cref="IsLightDismissEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsLightDismissEnabledProperty =
AvaloniaProperty.Register<AbstractPopup, bool>(nameof(IsLightDismissEnabled));
/// <summary>
/// Defines the <see cref="VerticalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> VerticalOffsetProperty =
AvaloniaProperty.Register<AbstractPopup, double>(nameof(VerticalOffset));
/// <summary>
/// Defines the <see cref="Topmost"/> property.
/// </summary>
public static readonly StyledProperty<bool> TopmostProperty =
AvaloniaProperty.Register<AbstractPopup, bool>(nameof(Topmost));
/// <summary>
/// Raised when the popup closes.
/// </summary>
public event EventHandler<EventArgs>? Closed;
/// <summary>
/// Raised when the popup opens.
/// </summary>
public event EventHandler? Opened;
internal event EventHandler<CancelEventArgs>? Closing;
public IPopupHost? Host => _openState?.PopupHost;
/// <summary>
/// Gets or sets a hint to the window manager that a shadow should be added to the popup.
/// </summary>
public bool WindowManagerAddShadowHint
{
get => GetValue(WindowManagerAddShadowHintProperty);
set => SetValue(WindowManagerAddShadowHintProperty, value);
}
/// <summary>
/// Gets or sets the control to display in the popup.
/// </summary>
[Content]
public Control? Child
{
get => GetValue(ChildProperty);
set => SetValue(ChildProperty, value);
}
/// <summary>
/// Gets or sets a dependency resolver for the <see cref="PopupRoot"/>.
/// </summary>
/// <remarks>
/// This property allows a client to customize the behaviour of the popup by injecting
/// a specialized dependency resolver into the <see cref="PopupRoot"/>'s constructor.
/// </remarks>
public IAvaloniaDependencyResolver? DependencyResolver { get; set; }
/// <summary>
/// Gets or sets a value that determines whether the popup inherits the render transform
/// from its <see cref="PlacementTarget"/>. Defaults to false.
/// </summary>
public bool InheritsTransform
{
get => GetValue(InheritsTransformProperty);
set => SetValue(InheritsTransformProperty, value);
}
/// <summary>
/// Gets or sets a value that determines how the <see cref="AbstractPopup"/> can be dismissed.
/// </summary>
/// <remarks>
/// Light dismiss is when the user taps on any area other than the popup.
/// </remarks>
public bool IsLightDismissEnabled
{
get => GetValue(IsLightDismissEnabledProperty);
set => SetValue(IsLightDismissEnabledProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the popup is currently open.
/// </summary>
public bool IsOpen
{
get => GetValue(IsOpenProperty);
set => SetValue(IsOpenProperty, value);
}
/// <summary>
/// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="Placement"/>
/// is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupAnchor PlacementAnchor
{
get => GetValue(PlacementAnchorProperty);
set => SetValue(PlacementAnchorProperty, value);
}
/// <summary>
/// Gets or sets a value describing how the popup position will be adjusted if the
/// unadjusted position would result in the popup being partly constrained.
/// </summary>
public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
{
get => GetValue(PlacementConstraintAdjustmentProperty);
set => SetValue(PlacementConstraintAdjustmentProperty, value);
}
/// <summary>
/// Gets or sets a value which defines in what direction the popup should open
/// when <see cref="Placement"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupGravity PlacementGravity
{
get => GetValue(PlacementGravityProperty);
set => SetValue(PlacementGravityProperty, value);
}
/// <inheritdoc cref="Placement"/>
[Obsolete("Use the Placement property instead."), EditorBrowsable(EditorBrowsableState.Never)]
public PlacementMode PlacementMode
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <summary>
/// Gets or sets the desired placement of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public PlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <summary>
/// Gets or sets the the anchor rectangle within the parent that the popup will be placed
/// relative to when <see cref="Placement"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
/// <remarks>
/// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
/// which the popup will be opened, with <see cref="PlacementAnchor"/> determining which edge
/// of the placement target is used.
///
/// If unset, the anchor rectangle will be the bounds of the <see cref="PlacementTarget"/>.
/// </remarks>
public Rect? PlacementRect
{
get => GetValue(PlacementRectProperty);
set => SetValue(PlacementRectProperty, value);
}
/// <summary>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
[ResolveByName]
public Control? PlacementTarget
{
get => GetValue(PlacementTargetProperty);
set => SetValue(PlacementTargetProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the event that closes the popup is passed
/// through to the parent window.
/// </summary>
/// <remarks>
/// When <see cref="IsLightDismissEnabled"/> is set to true, clicks outside the the popup
/// cause the popup to close. When <see cref="OverlayDismissEventPassThrough"/> is set to
/// false, these clicks will be handled by the popup and not be registered by the parent
/// window. When set to true, the events will be passed through to the parent window.
/// </remarks>
public bool OverlayDismissEventPassThrough
{
get => GetValue(OverlayDismissEventPassThroughProperty);
set => SetValue(OverlayDismissEventPassThroughProperty, value);
}
/// <summary>
/// Gets or sets an element that should receive pointer input events even when underneath
/// the popup's overlay.
/// </summary>
public IInputElement? OverlayInputPassThroughElement
{
get => GetValue(OverlayInputPassThroughElementProperty);
set => SetValue(OverlayInputPassThroughElementProperty, value);
}
/// <summary>
/// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double HorizontalOffset
{
get => GetValue(HorizontalOffsetProperty);
set => SetValue(HorizontalOffsetProperty, value);
}
/// <summary>
/// Gets or sets the Vertical offset of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double VerticalOffset
{
get => GetValue(VerticalOffsetProperty);
set => SetValue(VerticalOffsetProperty, value);
}
/// <summary>
/// Gets or sets whether this popup appears on top of all other windows
/// </summary>
public bool Topmost
{
get => GetValue(TopmostProperty);
set => SetValue(TopmostProperty, value);
}
IPopupHost? IPopupHostProvider.PopupHost => Host;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
private bool _isOpenRequested;
private bool _ignoreIsOpenChanged;
private PopupOpenState? _openState;
private Action<IPopupHost?>? _popupHostChangedHandler;
public event EventHandler<EventArgs>? PopupHostCreated;
public event EventHandler<EventArgs>? AboutToClosing;
private static readonly FieldInfo ManagedPopupPositionerPopupInfo;
protected IManagedPopupPositionerPopup? _managedPopupPositioner; // 在弹窗有效期获取
protected internal WeakReference<IPopupHost>? _popupHost;
static AbstractPopup()
{
ManagedPopupPositionerPopupInfo = typeof(ManagedPopupPositioner).GetField("_popup",
BindingFlags.Instance | BindingFlags.NonPublic)!;
}
public AbstractPopup()
{
(this as IPopupHostProvider).PopupHostChanged += HandlePopupChanged;
}
private void HandlePopupChanged(IPopupHost? host)
{
if (host is null) {
_popupHost = null;
NotifyClosed();
}
IsHitTestVisibleProperty.OverrideDefaultValue<AbstractPopup>(false);
ChildProperty.Changed.AddClassHandler<AbstractPopup>((x, e) => x.ChildChanged(e));
IsOpenProperty.Changed.AddClassHandler<AbstractPopup>(
(x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs<bool>)e));
VerticalOffsetProperty.Changed.AddClassHandler<AbstractPopup>((x, _) => x.HandlePositionChange());
HorizontalOffsetProperty.Changed.AddClassHandler<AbstractPopup>((x, _) => x.HandlePositionChange());
}
protected internal virtual void NotifyHostPositionUpdated(IPopupHost popupHost, Control placementTarget)
{
if (_popupHost is null) {
_popupHost = new WeakReference<IPopupHost>(popupHost);
NotifyPopupHostCreated(popupHost);
}
}
// 开始定位 Host 窗口
@ -67,6 +378,548 @@ public abstract class AbstractPopup : AvaloniaPopup
protected virtual void NotifyClosed() {}
protected internal virtual void NotifyPopupRootAboutToShow(PopupRoot popupRoot) {}
/// <summary>
/// Opens the popup.
/// </summary>
public void Open()
{
// AbstractPopup is currently open
if (_openState != null) {
return;
}
var placementTarget = PlacementTarget ?? this.FindLogicalAncestorOfType<Control>();
if (placementTarget == null) {
_isOpenRequested = true;
return;
}
var topLevel = TopLevel.GetTopLevel(placementTarget);
if (topLevel == null) {
_isOpenRequested = true;
return;
}
_isOpenRequested = false;
var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
NotifyPopupHostCreated(popupHost);
var handlerCleanup = new CompositeDisposable(7);
UpdateHostSizing(popupHost, topLevel, placementTarget);
popupHost.Topmost = Topmost;
popupHost.SetChild(Child);
((ISetLogicalParent)popupHost).SetParent(this);
if (InheritsTransform) {
TransformTrackingHelper.Track(placementTarget, PlacementTargetTransformChanged)
.DisposeWith(handlerCleanup);
} else {
popupHost.Transform = null;
}
if (popupHost is PopupRoot topLevelPopup) {
topLevelPopup
.Bind(
ThemeVariantScope.ActualThemeVariantProperty,
this.GetBindingObservable(ThemeVariantScope.ActualThemeVariantProperty))
.DisposeWith(handlerCleanup);
}
UpdateHostPosition(popupHost, placementTarget);
SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,
(x, handler) => x.TemplateApplied += handler,
(x, handler) => x.TemplateApplied -= handler).DisposeWith(handlerCleanup);
if (topLevel is Avalonia.Controls.Window window && window.PlatformImpl != null) {
SubscribeToEventHandler<Avalonia.Controls.Window, EventHandler>(window, WindowDeactivated,
(x, handler) => x.Deactivated += handler,
(x, handler) => x.Deactivated -= handler)
.DisposeWith(handlerCleanup);
SubscribeToEventHandler<IWindowImpl, Action>(window.PlatformImpl, WindowLostFocus,
(x, handler) => x.LostFocus += handler,
(x, handler) => x.LostFocus -= handler)
.DisposeWith(handlerCleanup);
// Recalculate popup position on parent moved/resized, but not if placement was on pointer
if (Placement != PlacementMode.Pointer) {
SubscribeToEventHandler<IWindowImpl, Action<PixelPoint>>(window.PlatformImpl, WindowPositionChanged,
(x, handler) => x.PositionChanged += handler,
(x, handler) => x.PositionChanged -= handler)
.DisposeWith(handlerCleanup);
if (placementTarget is Layoutable layoutTarget) {
// If the placement target is moved, update the popup position
SubscribeToEventHandler<Layoutable, EventHandler>(layoutTarget, PlacementTargetLayoutUpdated,
(x, handler) => x.LayoutUpdated += handler,
(x, handler) => x.LayoutUpdated -= handler)
.DisposeWith(handlerCleanup);
}
}
} else if (topLevel is PopupRoot parentPopupRoot) {
SubscribeToEventHandler<PopupRoot, EventHandler<PixelPointEventArgs>>(
parentPopupRoot, ParentPopupPositionChanged,
(x, handler) => x.PositionChanged += handler,
(x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup);
if (parentPopupRoot.Parent is AbstractPopup popup) {
SubscribeToEventHandler<AbstractPopup, EventHandler<EventArgs>>(popup, ParentClosed,
(x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler)
.DisposeWith(handlerCleanup);
}
}
var inputManager = AvaloniaLocator.Current.GetService<IInputManager>();
inputManager?.Process.Subscribe(ListenForNonClientClick).DisposeWith(handlerCleanup);
var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state =>
{
state.handlerCleanup.Dispose();
state.popupHost.SetChild(null);
state.popupHost.Hide();
((ISetLogicalParent)state.popupHost).SetParent(null);
state.popupHost.Dispose();
});
if (IsLightDismissEnabled) {
var dismissLayer = LightDismissOverlayLayer.GetLightDismissOverlayLayer(placementTarget);
if (dismissLayer != null) {
dismissLayer.IsVisible = true;
dismissLayer.InputPassThroughElement = OverlayInputPassThroughElement;
Disposable.Create(() =>
{
dismissLayer.IsVisible = false;
dismissLayer.InputPassThroughElement = null;
}).DisposeWith(handlerCleanup);
SubscribeToEventHandler<LightDismissOverlayLayer, EventHandler<PointerPressedEventArgs>>(
dismissLayer,
PointerPressedDismissOverlay,
(x, handler) => x.PointerPressed += handler,
(x, handler) => x.PointerPressed -= handler).DisposeWith(handlerCleanup);
}
}
_openState = new PopupOpenState(placementTarget, topLevel, popupHost, cleanupPopup);
WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint);
popupHost.Show();
using (BeginIgnoringIsOpen()) {
SetCurrentValue(IsOpenProperty, true);
}
Opened?.Invoke(this, EventArgs.Empty);
_popupHostChangedHandler?.Invoke(Host);
}
/// <summary>
/// Closes the popup.
/// </summary>
public void Close() => CloseCore();
/// <summary>
/// Measures the control.
/// </summary>
/// <param name="availableSize">The available size for the control.</param>
/// <returns>A size of 0,0 as AbstractPopup itself takes up no space.</returns>
protected override Size MeasureCore(Size availableSize)
{
return new Size();
}
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (_isOpenRequested) {
Open();
}
}
/// <inheritdoc/>
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
Close();
}
/// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (_openState is not null) {
if (change.Property == WidthProperty ||
change.Property == MinWidthProperty ||
change.Property == MaxWidthProperty ||
change.Property == HeightProperty ||
change.Property == MinHeightProperty ||
change.Property == MaxHeightProperty) {
UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget);
} else if (change.Property == PlacementTargetProperty ||
change.Property == PlacementProperty ||
change.Property == HorizontalOffsetProperty ||
change.Property == VerticalOffsetProperty ||
change.Property == PlacementAnchorProperty ||
change.Property == PlacementConstraintAdjustmentProperty ||
change.Property == PlacementRectProperty) {
if (change.Property == PlacementTargetProperty) {
var newTarget = change.GetNewValue<Control?>() ?? this.FindLogicalAncestorOfType<Control>();
if (newTarget is null || newTarget.GetVisualRoot() != _openState.TopLevel) {
Close();
return;
}
_openState.PlacementTarget = newTarget;
}
UpdateHostPosition(_openState.PopupHost, _openState.PlacementTarget);
} else if (change.Property == TopmostProperty) {
_openState.PopupHost.Topmost = change.GetNewValue<bool>();
}
}
}
private void UpdateHostPosition(IPopupHost popupHost, Control placementTarget)
{
NotifyAboutToUpdateHostPosition(popupHost, placementTarget);
popupHost.ConfigurePosition(
placementTarget,
Placement,
new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor,
PlacementGravity,
PlacementConstraintAdjustment,
PlacementRect ?? new Rect(default, placementTarget.Bounds.Size));
NotifyHostPositionUpdated(popupHost, placementTarget);
}
private void UpdateHostSizing(IPopupHost popupHost, TopLevel topLevel, Control placementTarget)
{
var scaleX = 1.0;
var scaleY = 1.0;
if (InheritsTransform && placementTarget.TransformToVisual(topLevel) is { } m) {
scaleX = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12);
scaleY = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12);
// Ideally we'd only assign a ScaleTransform here when the scale != 1, but there's
// an issue with LayoutTransformControl in that it sets its LayoutTransform property
// with LocalValue priority in ArrangeOverride in certain cases when LayoutTransform
// is null, which breaks TemplateBindings to this property. Offending commit/line:
//
// https://github.com/AvaloniaUI/Avalonia/commit/6fbe1c2180ef45a940e193f1b4637e64eaab80ed#diff-5344e793df13f462126a8153ef46c44194f244b6890f25501709bae51df97f82R54
popupHost.Transform = new ScaleTransform(scaleX, scaleY);
} else {
popupHost.Transform = null;
}
popupHost.Width = Width * scaleX;
popupHost.MinWidth = MinWidth * scaleX;
popupHost.MaxWidth = MaxWidth * scaleX;
popupHost.Height = Height * scaleY;
popupHost.MinHeight = MinHeight * scaleY;
popupHost.MaxHeight = MaxHeight * scaleY;
}
private void HandlePositionChange()
{
if (_openState != null) {
var placementTarget = PlacementTarget ?? this.FindLogicalAncestorOfType<Control>();
if (placementTarget == null) return;
_openState.PopupHost.ConfigurePosition(
placementTarget,
Placement,
new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor,
PlacementGravity,
PlacementConstraintAdjustment,
PlacementRect);
}
}
/// <inheritdoc />
protected override AutomationPeer OnCreateAutomationPeer()
{
return new PopupAutomationPeer(this);
}
private static IDisposable SubscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler,
Action<T, TEventHandler> subscribe,
Action<T, TEventHandler> unsubscribe)
{
subscribe(target, handler);
return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler));
}
private static void WindowManagerAddShadowHintChanged(IPopupHost host, bool hint)
{
if (host is PopupRoot pr) {
pr.WindowManagerAddShadowHint = hint;
}
}
/// <summary>
/// Called when the <see cref="IsOpen"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private void IsOpenChanged(AvaloniaPropertyChangedEventArgs<bool> e)
{
if (!_ignoreIsOpenChanged) {
if (e.NewValue.Value) {
Open();
} else {
Close();
}
}
}
/// <summary>
/// Called when the <see cref="Child"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private void ChildChanged(AvaloniaPropertyChangedEventArgs e)
{
LogicalChildren.Clear();
((ISetLogicalParent?)e.OldValue)?.SetParent(null);
if (e.NewValue != null) {
((ISetLogicalParent)e.NewValue).SetParent(this);
LogicalChildren.Add((ILogical)e.NewValue);
}
}
private void CloseCore()
{
var closingArgs = new CancelEventArgs();
Closing?.Invoke(this, closingArgs);
if (closingArgs.Cancel) {
return;
}
_isOpenRequested = false;
if (_openState is null) {
using (BeginIgnoringIsOpen()) {
SetCurrentValue(IsOpenProperty, false);
}
return;
}
_openState.Dispose();
_openState = null;
_popupHostChangedHandler?.Invoke(null);
using (BeginIgnoringIsOpen()) {
SetCurrentValue(IsOpenProperty, false);
}
NotifyClosed();
Closed?.Invoke(this, EventArgs.Empty);
}
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawPointerEventArgs;
if (IsLightDismissEnabled && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) {
CloseCore();
}
}
private void PointerPressedDismissOverlay(object? sender, PointerPressedEventArgs e)
{
if (IsLightDismissEnabled && e.Source is Visual v && !IsChildOrThis(v)) {
CloseCore();
if (OverlayDismissEventPassThrough) {
PassThroughEvent(e);
}
}
}
private static void PassThroughEvent(PointerPressedEventArgs e)
{
if (e.Source is LightDismissOverlayLayer layer &&
layer.GetVisualRoot() is InputElement root) {
var p = e.GetCurrentPoint(root);
var hit = root.InputHitTest(p.Position, x => x != layer);
if (hit != null) {
e.Pointer.Capture(hit);
hit.RaiseEvent(e);
e.Handled = true;
}
}
}
private void RootTemplateApplied(object? sender, TemplateAppliedEventArgs e)
{
if (_openState is null) {
return;
}
var popupHost = _openState.PopupHost;
popupHost.TemplateApplied -= RootTemplateApplied;
_openState.SetPresenterSubscription(null);
// If the AbstractPopup appears in a control template, then the child controls
// that appear in the popup host need to have their TemplatedParent
// properties set.
if (TemplatedParent != null && popupHost.Presenter is Control presenter) {
presenter.ApplyTemplate();
var presenterSubscription = presenter.GetObservable(ContentPresenter.ChildProperty)
.Subscribe(SetTemplatedParentAndApplyChildTemplates);
_openState.SetPresenterSubscription(presenterSubscription);
}
}
private void SetTemplatedParentAndApplyChildTemplates(Control? control)
{
if (control != null) {
TemplatedControlUtils.ApplyTemplatedParent(control, TemplatedParent);
}
}
private bool IsChildOrThis(Visual child)
{
if (_openState is null) {
return false;
}
var popupHost = _openState.PopupHost;
Visual? root = child.GetVisualRoot() as Visual;
while (root is IHostedVisualTreeRoot hostedRoot) {
if (root == popupHost) {
return true;
}
root = hostedRoot.Host?.GetVisualRoot() as Visual;
}
return false;
}
public bool IsInsidePopup(Visual visual)
{
if (_openState is null) {
return false;
}
var popupHost = _openState.PopupHost;
return ((Visual)popupHost).IsVisualAncestorOf(visual);
}
public bool IsPointerOverPopup => ((IInputElement?)_openState?.PopupHost)?.IsPointerOver ?? false;
private void WindowDeactivated(object? sender, EventArgs e)
{
if (IsLightDismissEnabled) {
Close();
}
}
private void ParentClosed(object? sender, EventArgs e)
{
if (IsLightDismissEnabled) {
Close();
}
}
private void PlacementTargetTransformChanged(Visual v, Matrix? matrix)
{
if (_openState is not null)
UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget);
}
private void WindowLostFocus()
{
if (IsLightDismissEnabled) Close();
}
private void WindowPositionChanged(PixelPoint pp) => HandlePositionChange();
private void PlacementTargetLayoutUpdated(object? src, EventArgs e) => HandlePositionChange();
private void ParentPopupPositionChanged(object? src, PixelPointEventArgs e) => HandlePositionChange();
private IgnoreIsOpenScope BeginIgnoringIsOpen()
{
return new IgnoreIsOpenScope(this);
}
private readonly struct IgnoreIsOpenScope : IDisposable
{
private readonly AbstractPopup _owner;
public IgnoreIsOpenScope(AbstractPopup owner)
{
_owner = owner;
_owner._ignoreIsOpenChanged = true;
}
public void Dispose()
{
_owner._ignoreIsOpenChanged = false;
}
}
private class PopupOpenState : IDisposable
{
private readonly IDisposable _cleanup;
private IDisposable? _presenterCleanup;
public PopupOpenState(Control placementTarget, TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup)
{
PlacementTarget = placementTarget;
TopLevel = topLevel;
PopupHost = popupHost;
_cleanup = cleanup;
}
public TopLevel TopLevel { get; }
public Control PlacementTarget { get; set; }
public IPopupHost PopupHost { get; }
public void SetPresenterSubscription(IDisposable? presenterCleanup)
{
_presenterCleanup?.Dispose();
_presenterCleanup = presenterCleanup;
}
public void Dispose()
{
_presenterCleanup?.Dispose();
_cleanup.Dispose();
}
}
}
public class PopupHostCreatedEventArgs : EventArgs

View File

@ -1,5 +1,4 @@
using System.Reactive.Disposables;
using System.Reflection;
using AtomUI.Styling;
using AtomUI.Utils;
using Avalonia;
@ -47,19 +46,15 @@ public class Popup : AbstractPopup
private set => SetAndRaise(IsFlippedProperty, ref _isFlipped, value);
}
private static readonly MethodInfo ConfigurePositionMethodInfo;
private PopupShadowLayer? _shadowLayer;
private CompositeDisposable? _compositeDisposable;
private bool _initialized;
static Popup()
{
var type = typeof(IPopupPositioner).Assembly.GetType("Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerExtensions")!;
ConfigurePositionMethodInfo = type.GetMethod("ConfigurePosition", BindingFlags.Public | BindingFlags.Static)!;
AffectsMeasure<Popup>(PlacementProperty);
AffectsMeasure<Popup>(PlacementAnchorProperty);
AffectsMeasure<Popup>(PlacementGravityProperty);
}
public Popup()
@ -199,31 +194,6 @@ public class Popup : AbstractPopup
var size = topLevel.ClientSize * topLevel.RenderScaling;
return new Rect(point.X, point.Y, size.Width, size.Height);
}
public static void ConfigurePosition(ref PopupPositionerParameters positionerParameters,
TopLevel topLevel,
Visual target, PlacementMode placement, Point offset,
PopupAnchor anchor, PopupGravity gravity,
PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect,
FlowDirection flowDirection)
{
var arguments = new object?[]
{
positionerParameters,
topLevel,
target,
placement,
offset,
anchor,
gravity,
constraintAdjustment,
rect,
flowDirection
};
ConfigurePositionMethodInfo.Invoke(null, arguments);
positionerParameters = (PopupPositionerParameters)arguments[0]!;
}
protected internal override void NotifyPopupRootAboutToShow(PopupRoot popupRoot)
{
@ -243,15 +213,15 @@ public class Popup : AbstractPopup
// 计算是否 flip
PopupPositionerParameters parameters = new PopupPositionerParameters();
var offset = new Point(HorizontalOffset, VerticalOffset);
ConfigurePosition(ref parameters, popupRoot.ParentTopLevel,
PlacementTarget!,
Placement,
offset,
PlacementAnchor,
PlacementGravity,
PopupPositionerConstraintAdjustment.All,
null,
FlowDirection);
parameters.ConfigurePosition(popupRoot.ParentTopLevel,
PlacementTarget!,
Placement,
offset,
PlacementAnchor,
PlacementGravity,
PopupPositionerConstraintAdjustment.All,
null,
FlowDirection);
Size popupSize;
// Popup.Child can't be null here, it was set in ShowAtCore.
@ -333,15 +303,15 @@ public class Popup : AbstractPopup
offset = new Point(offsetX, offsetY);
}
ConfigurePosition(ref parameters, parentTopLevel,
placementTarget,
placement,
offset,
placementAnchor,
placementGravity,
PopupPositionerConstraintAdjustment.All,
placementRect ?? new Rect(default, placementTarget.Bounds.Size),
flowDirection);
parameters.ConfigurePosition(parentTopLevel,
placementTarget,
placement,
offset,
placementAnchor,
placementGravity,
PopupPositionerConstraintAdjustment.All,
placementRect ?? new Rect(default, placementTarget.Bounds.Size),
flowDirection);
var positionInfo = new PopupPositionInfo();
positionInfo.EffectivePlacement = placement;

View File

@ -0,0 +1,46 @@
using Avalonia.Automation.Peers;
using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
namespace AtomUI.Controls;
public class PopupAutomationPeer : ControlAutomationPeer
{
public PopupAutomationPeer(AbstractPopup owner)
: base(owner)
{
owner.Opened += PopupOpenedClosed;
owner.Closed += PopupOpenedClosed;
}
protected override IReadOnlyList<AutomationPeer>? GetChildrenCore()
{
var host = (IPopupHostProvider)Owner;
return host.PopupHost is Control c ? new[] { GetOrCreate(c) } : null;
}
protected override bool IsContentElementCore() => false;
protected override bool IsControlElementCore() => false;
private void PopupOpenedClosed(object? sender, EventArgs e)
{
// This is golden. We're following WPF's automation peer API here where the
// parent of a peer is set when another peer returns it as a child. We want to
// add the popup root as a child of the popup, so we need to return it as a
// child right? Yeah except invalidating children doesn't automatically cause
// UIA to re-read the children meaning that the parent doesn't get set. So the
// MAIN MECHANISM FOR PARENTING CONTROLS IS BROKEN WITH THE ONLY AUTOMATION API
// IT WAS WRITTEN FOR. Luckily WPF provides an escape-hatch by exposing the
// TrySetParent API internally to work around this. We're exposing it publicly
// to shame whoever came up with this abomination of an API.
// TODO 用反射解决
// GetPopupRoot()?.TrySetParent(this);
// InvalidateChildren();
}
private AutomationPeer? GetPopupRoot()
{
var popupRoot = ((IPopupHostProvider)Owner).PopupHost as Control;
return popupRoot is object ? GetOrCreate(popupRoot) : null;
}
}

View File

@ -35,7 +35,7 @@ public sealed class PopupRoot : WindowBase, IHostedVisualTreeRoot, IDisposable,
/// </summary>
static PopupRoot()
{
BackgroundProperty.OverrideDefaultValue(typeof(PopupRoot), Brushes.White);
BackgroundProperty.OverrideDefaultValue(typeof(PopupRoot), Brushes.Transparent);
}
/// <summary>
@ -194,10 +194,10 @@ public sealed class PopupRoot : WindowBase, IHostedVisualTreeRoot, IDisposable,
return ClientSize;
}
// protected override AutomationPeer OnCreateAutomationPeer()
// {
// return new PopupRootAutomationPeer(this);
// }
protected override AutomationPeer OnCreateAutomationPeer()
{
return new PopupRootAutomationPeer(this);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
@ -208,4 +208,12 @@ public sealed class PopupRoot : WindowBase, IHostedVisualTreeRoot, IDisposable,
PlatformImpl?.SetWindowManagerAddShadowHint(change.GetNewValue<bool>());
}
}
public override void Show()
{
if (Parent is Popup popup) {
popup.NotifyPopupRootAboutToShow(this);
}
base.Show();
}
}

View File

@ -0,0 +1,39 @@
using Avalonia.Automation.Peers;
namespace AtomUI.Controls;
public class PopupRootAutomationPeer : WindowBaseAutomationPeer
{
public PopupRootAutomationPeer(PopupRoot owner)
: base(owner)
{
if (owner.IsVisible) {
StartTrackingFocus();
} else {
owner.Opened += OnOpened;
}
owner.Closed += OnClosed;
}
protected override bool IsContentElementCore() => false;
protected override bool IsControlElementCore() => false;
protected override AutomationPeer? GetParentCore()
{
var parent = base.GetParentCore();
return parent;
}
private void OnOpened(object? sender, EventArgs e)
{
((PopupRoot)Owner).Opened -= OnOpened;
StartTrackingFocus();
}
private void OnClosed(object? sender, EventArgs e)
{
((PopupRoot)Owner).Closed -= OnClosed;
StopTrackingFocus();
}
}

View File

@ -1,887 +0,0 @@
using System.ComponentModel;
using System.Reactive.Disposables;
using AtomUI.Controls.Utils;
using AtomUI.Input;
using AtomUI.Reactive;
using Avalonia;
using Avalonia.Automation.Peers;
using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace AtomUI.Controls;
/// <summary>
/// Displays a popup window.
/// </summary>
public class PopupX : Control, IPopupHostProvider
{
public static readonly StyledProperty<bool> WindowManagerAddShadowHintProperty =
AvaloniaProperty.Register<PopupX, bool>(nameof(WindowManagerAddShadowHint), false);
/// <summary>
/// Defines the <see cref="Child"/> property.
/// </summary>
public static readonly StyledProperty<Control?> ChildProperty =
AvaloniaProperty.Register<PopupX, Control?>(nameof(Child));
/// <summary>
/// Defines the <see cref="InheritsTransform"/> property.
/// </summary>
public static readonly StyledProperty<bool> InheritsTransformProperty =
AvaloniaProperty.Register<PopupX, bool>(nameof(InheritsTransform));
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsOpenProperty =
AvaloniaProperty.Register<PopupX, bool>(nameof(IsOpen));
/// <summary>
/// Defines the <see cref="PlacementAnchor"/> property.
/// </summary>
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
AvaloniaProperty.Register<PopupX, PopupAnchor>(nameof(PlacementAnchor));
/// <summary>
/// Defines the <see cref="PlacementConstraintAdjustment"/> property.
/// </summary>
public static readonly StyledProperty<PopupPositionerConstraintAdjustment> PlacementConstraintAdjustmentProperty =
AvaloniaProperty.Register<PopupX, PopupPositionerConstraintAdjustment>(
nameof(PlacementConstraintAdjustment),
PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY |
PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY |
PopupPositionerConstraintAdjustment.ResizeX | PopupPositionerConstraintAdjustment.ResizeY);
/// <summary>
/// Defines the <see cref="PlacementGravity"/> property.
/// </summary>
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
AvaloniaProperty.Register<PopupX, PopupGravity>(nameof(PlacementGravity));
/// <summary>
/// Defines the <see cref="Placement"/> property.
/// </summary>
public static readonly StyledProperty<PlacementMode> PlacementProperty =
AvaloniaProperty.Register<PopupX, PlacementMode>(nameof(Placement), defaultValue: PlacementMode.Bottom);
/// <summary>
/// Defines the <see cref="PlacementMode"/> property.
/// </summary>
[Obsolete("Use the Placement property instead."), EditorBrowsable(EditorBrowsableState.Never)]
public static readonly StyledProperty<PlacementMode> PlacementModeProperty = PlacementProperty;
/// <summary>
/// Defines the <see cref="PlacementRect"/> property.
/// </summary>
public static readonly StyledProperty<Rect?> PlacementRectProperty =
AvaloniaProperty.Register<PopupX, Rect?>(nameof(PlacementRect));
/// <summary>
/// Defines the <see cref="PlacementTarget"/> property.
/// </summary>
public static readonly StyledProperty<Control?> PlacementTargetProperty =
AvaloniaProperty.Register<PopupX, Control?>(nameof(PlacementTarget));
public static readonly StyledProperty<bool> OverlayDismissEventPassThroughProperty =
AvaloniaProperty.Register<PopupX, bool>(nameof(OverlayDismissEventPassThrough));
public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
AvaloniaProperty.Register<PopupX, IInputElement?>(nameof(OverlayInputPassThroughElement));
/// <summary>
/// Defines the <see cref="HorizontalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> HorizontalOffsetProperty =
AvaloniaProperty.Register<PopupX, double>(nameof(HorizontalOffset));
/// <summary>
/// Defines the <see cref="IsLightDismissEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsLightDismissEnabledProperty =
AvaloniaProperty.Register<PopupX, bool>(nameof(IsLightDismissEnabled));
/// <summary>
/// Defines the <see cref="VerticalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> VerticalOffsetProperty =
AvaloniaProperty.Register<PopupX, double>(nameof(VerticalOffset));
/// <summary>
/// Defines the <see cref="Topmost"/> property.
/// </summary>
public static readonly StyledProperty<bool> TopmostProperty =
AvaloniaProperty.Register<PopupX, bool>(nameof(Topmost));
private bool _isOpenRequested;
private bool _ignoreIsOpenChanged;
private PopupOpenState? _openState;
private Action<IPopupHost?>? _popupHostChangedHandler;
/// <summary>
/// Initializes static members of the <see cref="PopupX"/> class.
/// </summary>
static PopupX()
{
IsHitTestVisibleProperty.OverrideDefaultValue<PopupX>(false);
ChildProperty.Changed.AddClassHandler<PopupX>((x, e) => x.ChildChanged(e));
IsOpenProperty.Changed.AddClassHandler<PopupX>(
(x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs<bool>)e));
VerticalOffsetProperty.Changed.AddClassHandler<PopupX>((x, _) => x.HandlePositionChange());
HorizontalOffsetProperty.Changed.AddClassHandler<PopupX>((x, _) => x.HandlePositionChange());
}
/// <summary>
/// Raised when the popup closes.
/// </summary>
public event EventHandler<EventArgs>? Closed;
/// <summary>
/// Raised when the popup opens.
/// </summary>
public event EventHandler? Opened;
internal event EventHandler<CancelEventArgs>? Closing;
public IPopupHost? Host => _openState?.PopupHost;
/// <summary>
/// Gets or sets a hint to the window manager that a shadow should be added to the popup.
/// </summary>
public bool WindowManagerAddShadowHint
{
get => GetValue(WindowManagerAddShadowHintProperty);
set => SetValue(WindowManagerAddShadowHintProperty, value);
}
/// <summary>
/// Gets or sets the control to display in the popup.
/// </summary>
[Content]
public Control? Child
{
get => GetValue(ChildProperty);
set => SetValue(ChildProperty, value);
}
/// <summary>
/// Gets or sets a dependency resolver for the <see cref="PopupRoot"/>.
/// </summary>
/// <remarks>
/// This property allows a client to customize the behaviour of the popup by injecting
/// a specialized dependency resolver into the <see cref="PopupRoot"/>'s constructor.
/// </remarks>
public IAvaloniaDependencyResolver? DependencyResolver { get; set; }
/// <summary>
/// Gets or sets a value that determines whether the popup inherits the render transform
/// from its <see cref="PlacementTarget"/>. Defaults to false.
/// </summary>
public bool InheritsTransform
{
get => GetValue(InheritsTransformProperty);
set => SetValue(InheritsTransformProperty, value);
}
/// <summary>
/// Gets or sets a value that determines how the <see cref="PopupX"/> can be dismissed.
/// </summary>
/// <remarks>
/// Light dismiss is when the user taps on any area other than the popup.
/// </remarks>
public bool IsLightDismissEnabled
{
get => GetValue(IsLightDismissEnabledProperty);
set => SetValue(IsLightDismissEnabledProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the popup is currently open.
/// </summary>
public bool IsOpen
{
get => GetValue(IsOpenProperty);
set => SetValue(IsOpenProperty, value);
}
/// <summary>
/// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="Placement"/>
/// is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupAnchor PlacementAnchor
{
get => GetValue(PlacementAnchorProperty);
set => SetValue(PlacementAnchorProperty, value);
}
/// <summary>
/// Gets or sets a value describing how the popup position will be adjusted if the
/// unadjusted position would result in the popup being partly constrained.
/// </summary>
public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
{
get => GetValue(PlacementConstraintAdjustmentProperty);
set => SetValue(PlacementConstraintAdjustmentProperty, value);
}
/// <summary>
/// Gets or sets a value which defines in what direction the popup should open
/// when <see cref="Placement"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupGravity PlacementGravity
{
get => GetValue(PlacementGravityProperty);
set => SetValue(PlacementGravityProperty, value);
}
/// <inheritdoc cref="Placement"/>
[Obsolete("Use the Placement property instead."), EditorBrowsable(EditorBrowsableState.Never)]
public PlacementMode PlacementMode
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <summary>
/// Gets or sets the desired placement of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public PlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <summary>
/// Gets or sets the the anchor rectangle within the parent that the popup will be placed
/// relative to when <see cref="Placement"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
/// <remarks>
/// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
/// which the popup will be opened, with <see cref="PlacementAnchor"/> determining which edge
/// of the placement target is used.
///
/// If unset, the anchor rectangle will be the bounds of the <see cref="PlacementTarget"/>.
/// </remarks>
public Rect? PlacementRect
{
get => GetValue(PlacementRectProperty);
set => SetValue(PlacementRectProperty, value);
}
/// <summary>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
[ResolveByName]
public Control? PlacementTarget
{
get => GetValue(PlacementTargetProperty);
set => SetValue(PlacementTargetProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the event that closes the popup is passed
/// through to the parent window.
/// </summary>
/// <remarks>
/// When <see cref="IsLightDismissEnabled"/> is set to true, clicks outside the the popup
/// cause the popup to close. When <see cref="OverlayDismissEventPassThrough"/> is set to
/// false, these clicks will be handled by the popup and not be registered by the parent
/// window. When set to true, the events will be passed through to the parent window.
/// </remarks>
public bool OverlayDismissEventPassThrough
{
get => GetValue(OverlayDismissEventPassThroughProperty);
set => SetValue(OverlayDismissEventPassThroughProperty, value);
}
/// <summary>
/// Gets or sets an element that should receive pointer input events even when underneath
/// the popup's overlay.
/// </summary>
public IInputElement? OverlayInputPassThroughElement
{
get => GetValue(OverlayInputPassThroughElementProperty);
set => SetValue(OverlayInputPassThroughElementProperty, value);
}
/// <summary>
/// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double HorizontalOffset
{
get => GetValue(HorizontalOffsetProperty);
set => SetValue(HorizontalOffsetProperty, value);
}
/// <summary>
/// Gets or sets the Vertical offset of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double VerticalOffset
{
get => GetValue(VerticalOffsetProperty);
set => SetValue(VerticalOffsetProperty, value);
}
/// <summary>
/// Gets or sets whether this popup appears on top of all other windows
/// </summary>
public bool Topmost
{
get => GetValue(TopmostProperty);
set => SetValue(TopmostProperty, value);
}
IPopupHost? IPopupHostProvider.PopupHost => Host;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
/// <summary>
/// Opens the popup.
/// </summary>
public void Open()
{
// PopupX is currently open
if (_openState != null) {
return;
}
var placementTarget = PlacementTarget ?? this.FindLogicalAncestorOfType<Control>();
if (placementTarget == null) {
_isOpenRequested = true;
return;
}
var topLevel = TopLevel.GetTopLevel(placementTarget);
if (topLevel == null) {
_isOpenRequested = true;
return;
}
_isOpenRequested = false;
var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
var handlerCleanup = new CompositeDisposable(7);
UpdateHostSizing(popupHost, topLevel, placementTarget);
popupHost.Topmost = Topmost;
popupHost.SetChild(Child);
((ISetLogicalParent)popupHost).SetParent(this);
if (InheritsTransform) {
TransformTrackingHelper.Track(placementTarget, PlacementTargetTransformChanged)
.DisposeWith(handlerCleanup);
} else {
popupHost.Transform = null;
}
if (popupHost is PopupRoot topLevelPopup) {
topLevelPopup
.Bind(
ThemeVariantScope.ActualThemeVariantProperty,
this.GetBindingObservable(ThemeVariantScope.ActualThemeVariantProperty))
.DisposeWith(handlerCleanup);
}
UpdateHostPosition(popupHost, placementTarget);
SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,
(x, handler) => x.TemplateApplied += handler,
(x, handler) => x.TemplateApplied -= handler).DisposeWith(handlerCleanup);
if (topLevel is Avalonia.Controls.Window window && window.PlatformImpl != null) {
SubscribeToEventHandler<Avalonia.Controls.Window, EventHandler>(window, WindowDeactivated,
(x, handler) => x.Deactivated += handler,
(x, handler) => x.Deactivated -= handler)
.DisposeWith(handlerCleanup);
SubscribeToEventHandler<IWindowImpl, Action>(window.PlatformImpl, WindowLostFocus,
(x, handler) => x.LostFocus += handler,
(x, handler) => x.LostFocus -= handler)
.DisposeWith(handlerCleanup);
// Recalculate popup position on parent moved/resized, but not if placement was on pointer
if (Placement != PlacementMode.Pointer) {
SubscribeToEventHandler<IWindowImpl, Action<PixelPoint>>(window.PlatformImpl, WindowPositionChanged,
(x, handler) => x.PositionChanged += handler,
(x, handler) => x.PositionChanged -= handler)
.DisposeWith(handlerCleanup);
if (placementTarget is Layoutable layoutTarget) {
// If the placement target is moved, update the popup position
SubscribeToEventHandler<Layoutable, EventHandler>(layoutTarget, PlacementTargetLayoutUpdated,
(x, handler) => x.LayoutUpdated += handler,
(x, handler) => x.LayoutUpdated -= handler)
.DisposeWith(handlerCleanup);
}
}
} else if (topLevel is PopupRoot parentPopupRoot) {
SubscribeToEventHandler<PopupRoot, EventHandler<PixelPointEventArgs>>(
parentPopupRoot, ParentPopupPositionChanged,
(x, handler) => x.PositionChanged += handler,
(x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup);
if (parentPopupRoot.Parent is PopupX popup) {
SubscribeToEventHandler<PopupX, EventHandler<EventArgs>>(popup, ParentClosed,
(x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler)
.DisposeWith(handlerCleanup);
}
}
var inputManager = AvaloniaLocator.Current.GetService<IInputManager>();
inputManager?.Process.Subscribe(ListenForNonClientClick).DisposeWith(handlerCleanup);
var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state =>
{
state.handlerCleanup.Dispose();
state.popupHost.SetChild(null);
state.popupHost.Hide();
((ISetLogicalParent)state.popupHost).SetParent(null);
state.popupHost.Dispose();
});
if (IsLightDismissEnabled) {
var dismissLayer = LightDismissOverlayLayer.GetLightDismissOverlayLayer(placementTarget);
if (dismissLayer != null) {
dismissLayer.IsVisible = true;
dismissLayer.InputPassThroughElement = OverlayInputPassThroughElement;
Disposable.Create(() =>
{
dismissLayer.IsVisible = false;
dismissLayer.InputPassThroughElement = null;
}).DisposeWith(handlerCleanup);
SubscribeToEventHandler<LightDismissOverlayLayer, EventHandler<PointerPressedEventArgs>>(
dismissLayer,
PointerPressedDismissOverlay,
(x, handler) => x.PointerPressed += handler,
(x, handler) => x.PointerPressed -= handler).DisposeWith(handlerCleanup);
}
}
_openState = new PopupOpenState(placementTarget, topLevel, popupHost, cleanupPopup);
WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint);
popupHost.Show();
using (BeginIgnoringIsOpen()) {
SetCurrentValue(IsOpenProperty, true);
}
Opened?.Invoke(this, EventArgs.Empty);
_popupHostChangedHandler?.Invoke(Host);
}
/// <summary>
/// Closes the popup.
/// </summary>
public void Close() => CloseCore();
/// <summary>
/// Measures the control.
/// </summary>
/// <param name="availableSize">The available size for the control.</param>
/// <returns>A size of 0,0 as PopupX itself takes up no space.</returns>
protected override Size MeasureCore(Size availableSize)
{
return new Size();
}
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (_isOpenRequested) {
Open();
}
}
/// <inheritdoc/>
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
Close();
}
/// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (_openState is not null) {
if (change.Property == WidthProperty ||
change.Property == MinWidthProperty ||
change.Property == MaxWidthProperty ||
change.Property == HeightProperty ||
change.Property == MinHeightProperty ||
change.Property == MaxHeightProperty) {
UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget);
} else if (change.Property == PlacementTargetProperty ||
change.Property == PlacementProperty ||
change.Property == HorizontalOffsetProperty ||
change.Property == VerticalOffsetProperty ||
change.Property == PlacementAnchorProperty ||
change.Property == PlacementConstraintAdjustmentProperty ||
change.Property == PlacementRectProperty) {
if (change.Property == PlacementTargetProperty) {
var newTarget = change.GetNewValue<Control?>() ?? this.FindLogicalAncestorOfType<Control>();
if (newTarget is null || newTarget.GetVisualRoot() != _openState.TopLevel) {
Close();
return;
}
_openState.PlacementTarget = newTarget;
}
UpdateHostPosition(_openState.PopupHost, _openState.PlacementTarget);
} else if (change.Property == TopmostProperty) {
_openState.PopupHost.Topmost = change.GetNewValue<bool>();
}
}
}
private void UpdateHostPosition(IPopupHost popupHost, Control placementTarget)
{
popupHost.ConfigurePosition(
placementTarget,
Placement,
new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor,
PlacementGravity,
PlacementConstraintAdjustment,
PlacementRect ?? new Rect(default, placementTarget.Bounds.Size));
}
private void UpdateHostSizing(IPopupHost popupHost, TopLevel topLevel, Control placementTarget)
{
var scaleX = 1.0;
var scaleY = 1.0;
if (InheritsTransform && placementTarget.TransformToVisual(topLevel) is { } m) {
scaleX = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12);
scaleY = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12);
// Ideally we'd only assign a ScaleTransform here when the scale != 1, but there's
// an issue with LayoutTransformControl in that it sets its LayoutTransform property
// with LocalValue priority in ArrangeOverride in certain cases when LayoutTransform
// is null, which breaks TemplateBindings to this property. Offending commit/line:
//
// https://github.com/AvaloniaUI/Avalonia/commit/6fbe1c2180ef45a940e193f1b4637e64eaab80ed#diff-5344e793df13f462126a8153ef46c44194f244b6890f25501709bae51df97f82R54
popupHost.Transform = new ScaleTransform(scaleX, scaleY);
} else {
popupHost.Transform = null;
}
popupHost.Width = Width * scaleX;
popupHost.MinWidth = MinWidth * scaleX;
popupHost.MaxWidth = MaxWidth * scaleX;
popupHost.Height = Height * scaleY;
popupHost.MinHeight = MinHeight * scaleY;
popupHost.MaxHeight = MaxHeight * scaleY;
}
private void HandlePositionChange()
{
if (_openState != null) {
var placementTarget = PlacementTarget ?? this.FindLogicalAncestorOfType<Control>();
if (placementTarget == null) return;
_openState.PopupHost.ConfigurePosition(
placementTarget,
Placement,
new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor,
PlacementGravity,
PlacementConstraintAdjustment,
PlacementRect);
}
}
/// <inheritdoc />
// protected override AutomationPeer OnCreateAutomationPeer()
// {
// return new PopupAutomationPeer(this);
// }
private static IDisposable SubscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler,
Action<T, TEventHandler> subscribe,
Action<T, TEventHandler> unsubscribe)
{
subscribe(target, handler);
return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler));
}
private static void WindowManagerAddShadowHintChanged(IPopupHost host, bool hint)
{
if (host is PopupRoot pr) {
pr.WindowManagerAddShadowHint = hint;
}
}
/// <summary>
/// Called when the <see cref="IsOpen"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private void IsOpenChanged(AvaloniaPropertyChangedEventArgs<bool> e)
{
if (!_ignoreIsOpenChanged) {
if (e.NewValue.Value) {
Open();
} else {
Close();
}
}
}
/// <summary>
/// Called when the <see cref="Child"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private void ChildChanged(AvaloniaPropertyChangedEventArgs e)
{
LogicalChildren.Clear();
((ISetLogicalParent?)e.OldValue)?.SetParent(null);
if (e.NewValue != null) {
((ISetLogicalParent)e.NewValue).SetParent(this);
LogicalChildren.Add((ILogical)e.NewValue);
}
}
private void CloseCore()
{
var closingArgs = new CancelEventArgs();
Closing?.Invoke(this, closingArgs);
if (closingArgs.Cancel) {
return;
}
_isOpenRequested = false;
if (_openState is null) {
using (BeginIgnoringIsOpen()) {
SetCurrentValue(IsOpenProperty, false);
}
return;
}
_openState.Dispose();
_openState = null;
_popupHostChangedHandler?.Invoke(null);
using (BeginIgnoringIsOpen()) {
SetCurrentValue(IsOpenProperty, false);
}
Closed?.Invoke(this, EventArgs.Empty);
}
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawPointerEventArgs;
if (IsLightDismissEnabled && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) {
CloseCore();
}
}
private void PointerPressedDismissOverlay(object? sender, PointerPressedEventArgs e)
{
if (IsLightDismissEnabled && e.Source is Visual v && !IsChildOrThis(v)) {
CloseCore();
if (OverlayDismissEventPassThrough) {
PassThroughEvent(e);
}
}
}
private static void PassThroughEvent(PointerPressedEventArgs e)
{
if (e.Source is LightDismissOverlayLayer layer &&
layer.GetVisualRoot() is InputElement root) {
var p = e.GetCurrentPoint(root);
var hit = root.InputHitTest(p.Position, x => x != layer);
if (hit != null) {
e.Pointer.Capture(hit);
hit.RaiseEvent(e);
e.Handled = true;
}
}
}
private void RootTemplateApplied(object? sender, TemplateAppliedEventArgs e)
{
if (_openState is null) {
return;
}
var popupHost = _openState.PopupHost;
popupHost.TemplateApplied -= RootTemplateApplied;
_openState.SetPresenterSubscription(null);
// If the PopupX appears in a control template, then the child controls
// that appear in the popup host need to have their TemplatedParent
// properties set.
if (TemplatedParent != null && popupHost.Presenter is Control presenter) {
presenter.ApplyTemplate();
var presenterSubscription = presenter.GetObservable(ContentPresenter.ChildProperty)
.Subscribe(SetTemplatedParentAndApplyChildTemplates);
_openState.SetPresenterSubscription(presenterSubscription);
}
}
private void SetTemplatedParentAndApplyChildTemplates(Control? control)
{
if (control != null) {
TemplatedControlUtils.ApplyTemplatedParent(control, TemplatedParent);
}
}
private bool IsChildOrThis(Visual child)
{
if (_openState is null) {
return false;
}
var popupHost = _openState.PopupHost;
Visual? root = child.GetVisualRoot() as Visual;
while (root is IHostedVisualTreeRoot hostedRoot) {
if (root == popupHost) {
return true;
}
root = hostedRoot.Host?.GetVisualRoot() as Visual;
}
return false;
}
public bool IsInsidePopup(Visual visual)
{
if (_openState is null) {
return false;
}
var popupHost = _openState.PopupHost;
return ((Visual)popupHost).IsVisualAncestorOf(visual);
}
public bool IsPointerOverPopup => ((IInputElement?)_openState?.PopupHost)?.IsPointerOver ?? false;
private void WindowDeactivated(object? sender, EventArgs e)
{
if (IsLightDismissEnabled) {
Close();
}
}
private void ParentClosed(object? sender, EventArgs e)
{
if (IsLightDismissEnabled) {
Close();
}
}
private void PlacementTargetTransformChanged(Visual v, Matrix? matrix)
{
if (_openState is not null)
UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget);
}
private void WindowLostFocus()
{
if (IsLightDismissEnabled) Close();
}
private void WindowPositionChanged(PixelPoint pp) => HandlePositionChange();
private void PlacementTargetLayoutUpdated(object? src, EventArgs e) => HandlePositionChange();
private void ParentPopupPositionChanged(object? src, PixelPointEventArgs e) => HandlePositionChange();
private IgnoreIsOpenScope BeginIgnoringIsOpen()
{
return new IgnoreIsOpenScope(this);
}
private readonly struct IgnoreIsOpenScope : IDisposable
{
private readonly PopupX _owner;
public IgnoreIsOpenScope(PopupX owner)
{
_owner = owner;
_owner._ignoreIsOpenChanged = true;
}
public void Dispose()
{
_owner._ignoreIsOpenChanged = false;
}
}
private class PopupOpenState : IDisposable
{
private readonly IDisposable _cleanup;
private IDisposable? _presenterCleanup;
public PopupOpenState(Control placementTarget, TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup)
{
PlacementTarget = placementTarget;
TopLevel = topLevel;
PopupHost = popupHost;
_cleanup = cleanup;
}
public TopLevel TopLevel { get; }
public Control PlacementTarget { get; set; }
public IPopupHost PopupHost { get; }
public void SetPresenterSubscription(IDisposable? presenterCleanup)
{
_presenterCleanup?.Dispose();
_presenterCleanup = presenterCleanup;
}
public void Dispose()
{
_presenterCleanup?.Dispose();
_cleanup.Dispose();
}
}
}