|
|
|
@ -1,6 +1,9 @@ |
|
|
|
using System; |
|
|
|
using System.ComponentModel; |
|
|
|
using System.Linq; |
|
|
|
|
|
|
|
using Avalonia.Input; |
|
|
|
using Avalonia.Input.Platform; |
|
|
|
using Avalonia.Input.Raw; |
|
|
|
using Avalonia.Layout; |
|
|
|
using Avalonia.Logging; |
|
|
|
@ -49,6 +52,7 @@ namespace Avalonia.Controls.Primitives |
|
|
|
public static readonly AttachedProperty<FlyoutBase?> AttachedFlyoutProperty = |
|
|
|
AvaloniaProperty.RegisterAttached<FlyoutBase, Control, FlyoutBase?>("AttachedFlyout", null); |
|
|
|
|
|
|
|
private readonly Lazy<Popup> _popupLazy; |
|
|
|
private bool _isOpen; |
|
|
|
private Control? _target; |
|
|
|
private FlyoutShowMode _showMode = FlyoutShowMode.Standard; |
|
|
|
@ -56,7 +60,12 @@ namespace Avalonia.Controls.Primitives |
|
|
|
private PixelRect? _enlargePopupRectScreenPixelRect; |
|
|
|
private IDisposable? _transientDisposable; |
|
|
|
|
|
|
|
protected Popup? Popup { get; private set; } |
|
|
|
public FlyoutBase() |
|
|
|
{ |
|
|
|
_popupLazy = new Lazy<Popup>(() => CreatePopup()); |
|
|
|
} |
|
|
|
|
|
|
|
protected Popup Popup => _popupLazy.Value; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets whether this Flyout is currently Open
|
|
|
|
@ -142,18 +151,19 @@ namespace Avalonia.Controls.Primitives |
|
|
|
HideCore(); |
|
|
|
} |
|
|
|
|
|
|
|
protected virtual void HideCore(bool canCancel = true) |
|
|
|
/// <returns>True, if action was handled</returns>
|
|
|
|
protected virtual bool HideCore(bool canCancel = true) |
|
|
|
{ |
|
|
|
if (!IsOpen) |
|
|
|
{ |
|
|
|
return; |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (canCancel) |
|
|
|
{ |
|
|
|
if (CancelClosing()) |
|
|
|
{ |
|
|
|
return; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -166,34 +176,40 @@ namespace Avalonia.Controls.Primitives |
|
|
|
_enlargedPopupRect = null; |
|
|
|
_enlargePopupRectScreenPixelRect = null; |
|
|
|
|
|
|
|
if (Target != null) |
|
|
|
{ |
|
|
|
Target.DetachedFromVisualTree -= PlacementTarget_DetachedFromVisualTree; |
|
|
|
Target.KeyUp -= OnPlacementTargetOrPopupKeyUp; |
|
|
|
} |
|
|
|
|
|
|
|
OnClosed(); |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
protected virtual void ShowAtCore(Control placementTarget, bool showAtPointer = false) |
|
|
|
/// <returns>True, if action was handled</returns>
|
|
|
|
protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = false) |
|
|
|
{ |
|
|
|
if (placementTarget == null) |
|
|
|
throw new ArgumentNullException("placementTarget cannot be null"); |
|
|
|
|
|
|
|
if (Popup == null) |
|
|
|
{ |
|
|
|
InitPopup(); |
|
|
|
throw new ArgumentNullException(nameof(placementTarget)); |
|
|
|
} |
|
|
|
|
|
|
|
if (IsOpen) |
|
|
|
{ |
|
|
|
if (placementTarget == Target) |
|
|
|
{ |
|
|
|
return; |
|
|
|
return false; |
|
|
|
} |
|
|
|
else // Close before opening a new one
|
|
|
|
{ |
|
|
|
HideCore(false); |
|
|
|
_ = HideCore(false); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (CancelOpening()) |
|
|
|
{ |
|
|
|
return; |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (Popup.Parent != null && Popup.Parent != placementTarget) |
|
|
|
@ -212,11 +228,13 @@ namespace Avalonia.Controls.Primitives |
|
|
|
Popup.Child = CreatePresenter(); |
|
|
|
} |
|
|
|
|
|
|
|
OnOpening(); |
|
|
|
PositionPopup(showAtPointer); |
|
|
|
IsOpen = Popup.IsOpen = true; |
|
|
|
IsOpen = Popup.IsOpen = true; |
|
|
|
OnOpened(); |
|
|
|
|
|
|
|
|
|
|
|
placementTarget.DetachedFromVisualTree += PlacementTarget_DetachedFromVisualTree; |
|
|
|
placementTarget.KeyUp += OnPlacementTargetOrPopupKeyUp; |
|
|
|
|
|
|
|
if (ShowMode == FlyoutShowMode.Standard) |
|
|
|
{ |
|
|
|
// Try and focus content inside Flyout
|
|
|
|
@ -237,6 +255,13 @@ namespace Avalonia.Controls.Primitives |
|
|
|
{ |
|
|
|
_transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss); |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
private void PlacementTarget_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) |
|
|
|
{ |
|
|
|
_ = HideCore(false); |
|
|
|
} |
|
|
|
|
|
|
|
private void HandleTransientDismiss(RawInputEventArgs args) |
|
|
|
@ -255,7 +280,7 @@ namespace Avalonia.Controls.Primitives |
|
|
|
{ |
|
|
|
// 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); |
|
|
|
@ -295,9 +320,9 @@ namespace Avalonia.Controls.Primitives |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
protected virtual void OnOpening() |
|
|
|
protected virtual void OnOpening(CancelEventArgs args) |
|
|
|
{ |
|
|
|
Opening?.Invoke(this, null); |
|
|
|
Opening?.Invoke(this, args); |
|
|
|
} |
|
|
|
|
|
|
|
protected virtual void OnOpened() |
|
|
|
@ -321,15 +346,18 @@ namespace Avalonia.Controls.Primitives |
|
|
|
/// <returns></returns>
|
|
|
|
protected abstract Control CreatePresenter(); |
|
|
|
|
|
|
|
private void InitPopup() |
|
|
|
private Popup CreatePopup() |
|
|
|
{ |
|
|
|
Popup = new Popup(); |
|
|
|
Popup.WindowManagerAddShadowHint = false; |
|
|
|
Popup.IsLightDismissEnabled = true; |
|
|
|
|
|
|
|
Popup.Opened += OnPopupOpened; |
|
|
|
Popup.Closed += OnPopupClosed; |
|
|
|
Popup.Closing += OnPopupClosing; |
|
|
|
var popup = new Popup(); |
|
|
|
popup.WindowManagerAddShadowHint = false; |
|
|
|
popup.IsLightDismissEnabled = true; |
|
|
|
popup.OverlayDismissEventPassThrough = true; |
|
|
|
|
|
|
|
popup.Opened += OnPopupOpened; |
|
|
|
popup.Closed += OnPopupClosed; |
|
|
|
popup.Closing += OnPopupClosing; |
|
|
|
popup.KeyUp += OnPlacementTargetOrPopupKeyUp; |
|
|
|
return popup; |
|
|
|
} |
|
|
|
|
|
|
|
private void OnPopupOpened(object sender, EventArgs e) |
|
|
|
@ -339,7 +367,10 @@ namespace Avalonia.Controls.Primitives |
|
|
|
|
|
|
|
private void OnPopupClosing(object sender, CancelEventArgs e) |
|
|
|
{ |
|
|
|
e.Cancel = CancelClosing(); |
|
|
|
if (IsOpen) |
|
|
|
{ |
|
|
|
e.Cancel = CancelClosing(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void OnPopupClosed(object sender, EventArgs e) |
|
|
|
@ -347,10 +378,27 @@ namespace Avalonia.Controls.Primitives |
|
|
|
HideCore(false); |
|
|
|
} |
|
|
|
|
|
|
|
// 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 = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>(); |
|
|
|
|
|
|
|
if (keymap.OpenContextMenu.Any(k => k.Matches(e))) |
|
|
|
{ |
|
|
|
e.Handled = HideCore(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void PositionPopup(bool showAtPointer) |
|
|
|
{ |
|
|
|
Size sz; |
|
|
|
if(Popup.Child.DesiredSize == Size.Empty) |
|
|
|
// Popup.Child can't be null here, it was set in ShowAtCore.
|
|
|
|
if (Popup.Child!.DesiredSize == Size.Empty) |
|
|
|
{ |
|
|
|
// Popup may not have been shown yet. Measure content
|
|
|
|
sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness()); |
|
|
|
@ -377,19 +425,19 @@ namespace Avalonia.Controls.Primitives |
|
|
|
switch (Placement) |
|
|
|
{ |
|
|
|
case FlyoutPlacementMode.Top: //Above & centered
|
|
|
|
Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width-1, 1); |
|
|
|
Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width - 1, 1); |
|
|
|
Popup.PlacementGravity = PopupPositioning.PopupGravity.Top; |
|
|
|
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Top; |
|
|
|
break; |
|
|
|
|
|
|
|
case FlyoutPlacementMode.TopEdgeAlignedLeft: |
|
|
|
Popup.PlacementRect = new Rect(0, 0, 0, 0); |
|
|
|
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight; |
|
|
|
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight; |
|
|
|
break; |
|
|
|
|
|
|
|
case FlyoutPlacementMode.TopEdgeAlignedRight: |
|
|
|
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 10, 1); |
|
|
|
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft; |
|
|
|
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft; |
|
|
|
break; |
|
|
|
|
|
|
|
case FlyoutPlacementMode.RightEdgeAlignedTop: |
|
|
|
@ -461,46 +509,44 @@ namespace Avalonia.Controls.Primitives |
|
|
|
{ |
|
|
|
if (args.OldValue is FlyoutBase) |
|
|
|
{ |
|
|
|
c.PointerReleased -= OnControlWithContextFlyoutPointerReleased; |
|
|
|
c.ContextRequested -= OnControlContextRequested; |
|
|
|
} |
|
|
|
if (args.NewValue is FlyoutBase) |
|
|
|
{ |
|
|
|
c.PointerReleased += OnControlWithContextFlyoutPointerReleased; |
|
|
|
c.ContextRequested += OnControlContextRequested; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static void OnControlWithContextFlyoutPointerReleased(object sender, PointerReleasedEventArgs e) |
|
|
|
private static void OnControlContextRequested(object sender, ContextRequestedEventArgs e) |
|
|
|
{ |
|
|
|
if (sender is Control c) |
|
|
|
var control = (Control)sender; |
|
|
|
if (!e.Handled |
|
|
|
&& control.ContextFlyout is FlyoutBase flyout) |
|
|
|
{ |
|
|
|
if (e.InitialPressMouseButton == MouseButton.Right && |
|
|
|
e.GetCurrentPoint(c).Properties.PointerUpdateKind == PointerUpdateKind.RightButtonReleased) |
|
|
|
if (control.ContextMenu != null) |
|
|
|
{ |
|
|
|
if (c.ContextFlyout != null) |
|
|
|
{ |
|
|
|
if (c.ContextMenu != null) |
|
|
|
{ |
|
|
|
Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(c, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu"); |
|
|
|
return; |
|
|
|
} |
|
|
|
c.ContextFlyout.ShowAt(c, true); |
|
|
|
} |
|
|
|
Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(control, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu"); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// We do not support absolute popup positioning yet, so we ignore "point" at this moment.
|
|
|
|
var triggeredByPointerInput = e.TryGetPosition(null, out _); |
|
|
|
e.Handled = flyout.ShowAtCore(control, triggeredByPointerInput); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private bool CancelClosing() |
|
|
|
{ |
|
|
|
var eventArgs = new CancelEventArgs(); |
|
|
|
Closing?.Invoke(this, eventArgs); |
|
|
|
OnClosing(eventArgs); |
|
|
|
return eventArgs.Cancel; |
|
|
|
} |
|
|
|
|
|
|
|
private bool CancelOpening() |
|
|
|
{ |
|
|
|
var eventArgs = new CancelEventArgs(); |
|
|
|
Opening?.Invoke(this, eventArgs); |
|
|
|
OnOpening(eventArgs); |
|
|
|
return eventArgs.Cancel; |
|
|
|
} |
|
|
|
|
|
|
|
|