mirror of
https://gitee.com/chinware/atomui.git
synced 2024-11-29 18:38:16 +08:00
合并 Popup 相关代码
This commit is contained in:
parent
3d6df89e0b
commit
9c633e52ab
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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)!;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
|
46
src/AtomUI.Controls/Popup/PopupAutomationPeer.cs
Normal file
46
src/AtomUI.Controls/Popup/PopupAutomationPeer.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
39
src/AtomUI.Controls/Popup/PopupRootAutomationPeer.cs
Normal file
39
src/AtomUI.Controls/Popup/PopupRootAutomationPeer.cs
Normal 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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user