Add motion support for ContextMenu

This commit is contained in:
chinboy
2025-07-18 11:34:52 +08:00
parent 7e38143b3c
commit 852397c6e4
11 changed files with 172 additions and 44 deletions
+34 -9
View File
@@ -34,29 +34,37 @@ public class ContextMenu : AvaloniaContextMenu,
string IControlSharedTokenResourcesHost.TokenId => MenuToken.ID;
#endregion
private Popup? _popup;
public ContextMenu()
{
this.RegisterResources();
this.BindMotionProperties();
// 我们在这里有一次初始化的机会
var popup = new Popup
_popup = new Popup
{
WindowManagerAddShadowHint = false,
IsLightDismissEnabled = true,
OverlayDismissEventPassThrough = true
IsLightDismissEnabled = false,
OverlayDismissEventPassThrough = true,
IsDetectMouseClickEnabled = true
};
popup.Opened += this.CreateEventHandler("PopupOpened");
popup.Closed += this.CreateEventHandler<EventArgs>("PopupClosed");
_popup.Opened += this.CreateEventHandler("PopupOpened");
_popup.Closed += this.CreateEventHandler<EventArgs>("PopupClosed");
popup.AddClosingEventHandler(this.CreateEventHandler<CancelEventArgs>("PopupClosing")!);
popup.KeyUp += this.CreateEventHandler<KeyEventArgs>("PopupKeyUp");
_popup.AddClosingEventHandler(this.CreateEventHandler<CancelEventArgs>("PopupClosing")!);
_popup.KeyUp += this.CreateEventHandler<KeyEventArgs>("PopupKeyUp");
Closing += (sender, args) =>
{
args.Cancel = true;
};
this.SetPopup(popup);
this.SetPopup(_popup);
Opened += (sender, args) =>
{
_popup.SetIgnoreIsOpenChanged(true);
_popup.IsMotionAwareOpen = true;
};
}
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
@@ -68,4 +76,21 @@ public class ContextMenu : AvaloniaContextMenu,
base.PrepareContainerForItemOverride(container, item, index);
}
public override void Close()
{
_popup?.SetIgnoreIsOpenChanged(true);
base.Close();
if (_popup != null)
{
foreach (var childItem in Items)
{
if (childItem is MenuItem menuItem)
{
menuItem.IsSubMenuOpen = false;
}
}
_popup.IsMotionAwareOpen = false;
}
}
}
@@ -72,7 +72,7 @@
WindowManagerAddShadowHint="False"
IsLightDismissEnabled="False"
Placement="RightEdgeAlignedTop"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
IsMotionAwareOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Name="{x:Static atom:MenuItemThemeConstants.PopupFramePart}">
<atom:MenuScrollViewer IsScrollChainingEnabled="False">
<ItemsPresenter Name="{x:Static atom:MenuItemThemeConstants.ItemsPresenterPart}"
@@ -20,7 +20,7 @@
WindowManagerAddShadowHint="False"
IsLightDismissEnabled="False"
Placement="BottomEdgeAlignedLeft"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
IsMotionAwareOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Name="{x:Static atom:TopLevelMenuItemThemeConstants.PopupFramePart}">
<atom:MenuScrollViewer
IsScrollChainingEnabled="False">
@@ -206,7 +206,7 @@ internal class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler
}
_currentPressedIsValid = false;
if (e.InitialPressMouseButton == MouseButton.Left && item?.HasSubMenu == false)
if (e.InitialPressMouseButton == MouseButton.Left && item.HasSubMenu == false)
{
Click(item);
e.Handled = true;
+38 -7
View File
@@ -23,8 +23,11 @@ public abstract class NavMenuBase : SelectingItemsControl,
nameof(IsOpen),
o => o.IsOpen);
public static readonly StyledProperty<bool> IsMotionEnabledProperty
= MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<NavMenuBase>();
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<NavMenuBase>();
public static readonly StyledProperty<bool> IsAccordionModeProperty =
AvaloniaProperty.Register<NavMenu, bool>(nameof(IsAccordionMode), false);
private bool _isOpen;
@@ -40,6 +43,12 @@ public abstract class NavMenuBase : SelectingItemsControl,
set => SetValue(IsMotionEnabledProperty, value);
}
public bool IsAccordionMode
{
get => GetValue(IsAccordionModeProperty);
set => SetValue(IsAccordionModeProperty, value);
}
#endregion
#region 公共事件定义
@@ -106,6 +115,11 @@ public abstract class NavMenuBase : SelectingItemsControl,
/// </summary>
protected internal INavMenuInteractionHandler? InteractionHandler { get; protected set; }
static NavMenuBase()
{
NavMenuItem.SubmenuOpenedEvent.AddClassHandler<NavMenuBase>((x, e) => x.OnSubmenuOpened(e));
}
public NavMenuBase()
{
this.RegisterResources();
@@ -151,17 +165,34 @@ public abstract class NavMenuBase : SelectingItemsControl,
/// <param name="e">The event args.</param>
protected virtual void OnSubmenuOpened(RoutedEventArgs e)
{
if (e.Source is NavMenuItem menuItem && menuItem.Parent == this)
if (IsAccordionMode)
{
foreach (var child in this.GetLogicalChildren().OfType<NavMenuItem>())
if (e.Source is NavMenuItem menuItem && menuItem.Parent == this)
{
if (child != menuItem && child.IsSubMenuOpen)
foreach (var child in this.GetLogicalChildren().OfType<NavMenuItem>())
{
if (child != menuItem && child.IsSubMenuOpen)
{
child.IsSubMenuOpen = false;
}
}
}
}
IsOpen = true;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsAccordionModeProperty)
{
if (change.GetNewValue<bool>())
{
foreach (var child in this.GetLogicalChildren().OfType<NavMenuItem>())
{
child.IsSubMenuOpen = false;
}
}
}
IsOpen = true;
}
}
+1 -1
View File
@@ -22,6 +22,7 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Point = Avalonia.Point;
@@ -529,7 +530,6 @@ public class NavMenuItem : HeaderedSelectingItemsControl,
return;
}
}
SetCurrentValue(IsSubMenuOpenProperty, false);
}
@@ -59,7 +59,7 @@
WindowManagerAddShadowHint="False"
IsLightDismissEnabled="False"
Placement="RightEdgeAlignedTop"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
IsMotionAwareOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Name="{x:Static atom:NavMenuItemThemeConstants.PopupFramePart}">
<atom:MenuScrollViewer>
<ItemsPresenter Name="{x:Static atom:NavMenuItemThemeConstants.ItemsPresenterPart}"
@@ -42,7 +42,7 @@
WindowManagerAddShadowHint="False"
IsLightDismissEnabled="False"
Placement="BottomEdgeAlignedLeft"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
IsMotionAwareOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Name="{x:Static atom:TopLevelHorizontalNavMenuItemThemeConstants.PopupFramePart}"
MinWidth="{TemplateBinding EffectivePopupMinWidth}">
<atom:MenuScrollViewer>
+79 -15
View File
@@ -56,6 +56,12 @@ public class Popup : AvaloniaPopup,
public static readonly StyledProperty<bool> IsWaveSpiritEnabledProperty =
WaveSpiritAwareControlProperty.IsWaveSpiritEnabledProperty.AddOwner<Popup>();
public static readonly StyledProperty<bool> IsMotionAwareOpenProperty =
AvaloniaProperty.Register<Popup, bool>(nameof (IsMotionAwareOpen));
public static readonly StyledProperty<bool> ConfigureBlankMaskWhenMotionAwareOpenProperty =
AvaloniaProperty.Register<Popup, bool>(nameof (ConfigureBlankMaskWhenMotionAwareOpen), false);
public BoxShadows MaskShadows
{
@@ -106,6 +112,22 @@ public class Popup : AvaloniaPopup,
get => GetValue(CloseMotionProperty);
set => SetValue(CloseMotionProperty, value);
}
public bool IsMotionAwareOpen
{
get => GetValue(IsMotionAwareOpenProperty);
set => SetValue(IsMotionAwareOpenProperty, value);
}
/// <summary>
/// When the animation is turned on and off, whether to not make a complete mask after the animation ends, including the content
/// </summary>
public bool ConfigureBlankMaskWhenMotionAwareOpen
{
get => GetValue(ConfigureBlankMaskWhenMotionAwareOpenProperty);
set => SetValue(ConfigureBlankMaskWhenMotionAwareOpenProperty, value);
}
#endregion
#region 内部属性定义
@@ -134,6 +156,7 @@ public class Popup : AvaloniaPopup,
private IDisposable? _selfLightDismissDisposable;
private IManagedPopupPositionerPopup? _managedPopupPositioner;
private bool _isNeedDetectFlip = true;
private bool _ignoreIsOpenChanged;
// 在翻转之后或者恢复正常,会有属性的变动,在变动之后捕捉动画需要等一个事件循环,保证布局已经生效
private bool _isNeedWaitFlipSync;
@@ -246,7 +269,7 @@ public class Popup : AvaloniaPopup,
private void HandleMouseClick(RawInputEventArgs args)
{
if (!IsOpen)
if (!IsMotionAwareOpen)
{
return;
}
@@ -257,12 +280,15 @@ public class Popup : AvaloniaPopup,
{
if (pointerEventArgs.Type == RawPointerEventType.LeftButtonUp)
{
if (_firstDetected)
if (_firstDetected || _openAnimating)
{
_firstDetected = false;
if (_firstDetected)
{
_firstDetected = false;
}
return;
}
if (this is IPopupHostProvider popupHostProvider)
{
if (popupHostProvider.PopupHost != pointerEventArgs.Root)
@@ -510,7 +536,7 @@ public class Popup : AvaloniaPopup,
public void MotionAwareOpen(Action? opened = null)
{
// AbstractPopup is currently open
if (IsOpen || _openAnimating || _closeAnimating)
if (_openAnimating || _closeAnimating)
{
return;
}
@@ -520,6 +546,11 @@ public class Popup : AvaloniaPopup,
Open();
opened?.Invoke();
(Host as PopupRoot)?.PlatformImpl?.SetTopmost(true);
using (BeginIgnoringIsOpen())
{
SetCurrentValue(IsMotionAwareOpenProperty, true);
}
return;
}
@@ -551,6 +582,10 @@ public class Popup : AvaloniaPopup,
}, () =>
{
opened?.Invoke();
using (BeginIgnoringIsOpen())
{
SetCurrentValue(IsMotionAwareOpenProperty, true);
}
popupRoot.PlatformImpl?.SetTopmost(true);
_isNeedWaitFlipSync = false;
_openAnimating = false;
@@ -559,11 +594,7 @@ public class Popup : AvaloniaPopup,
RequestCloseWhereAnimationCompleted = false;
Dispatcher.UIThread.Post(() => { MotionAwareClose(); });
}
Dispatcher.UIThread.Post(() =>
{
popupRoot.Opacity = 1.0d;
});
Dispatcher.UIThread.Post(() => { popupRoot.Opacity = 1.0d; });
});
}
@@ -580,16 +611,16 @@ public class Popup : AvaloniaPopup,
return;
}
if (!IsOpen)
{
return;
}
Debug.Assert(_buddyLayer != null);
if (!IsMotionEnabled)
{
_isNeedDetectFlip = true;
Close();
closed?.Invoke();
using (BeginIgnoringIsOpen())
{
SetCurrentValue(IsMotionAwareOpenProperty, false);
}
return;
}
@@ -603,6 +634,10 @@ public class Popup : AvaloniaPopup,
closed?.Invoke();
_closeAnimating = false;
Close();
using (BeginIgnoringIsOpen())
{
SetCurrentValue(IsMotionAwareOpenProperty, false);
}
_isNeedDetectFlip = true;
});
}
@@ -614,6 +649,35 @@ public class Popup : AvaloniaPopup,
{
_isNeedWaitFlipSync = true;
}
else if (change.Property == IsMotionAwareOpenProperty)
{
if (!_ignoreIsOpenChanged)
{
if (change.GetNewValue<bool>())
{
MotionAwareOpen();
}
else
{
MotionAwareClose();
}
}
}
}
private IgnoreIsOpenScope BeginIgnoringIsOpen() => new IgnoreIsOpenScope(this);
private readonly struct IgnoreIsOpenScope : IDisposable
{
private readonly Popup _owner;
public IgnoreIsOpenScope(Popup owner)
{
_owner = owner;
_owner._ignoreIsOpenChanged = true;
}
public void Dispose() => _owner._ignoreIsOpenChanged = false;
}
}
@@ -625,4 +689,4 @@ public class PopupFlippedEventArgs : EventArgs
{
Flipped = flipped;
}
}
}
@@ -106,7 +106,14 @@ internal class PopupBuddyDecorator : SceneMotionActorControl
if (sender is ContentPresenter presenter)
{
ConfigureDecorator(presenter);
ConfigureBlankContentControl();
if (_popup.ConfigureBlankMaskWhenMotionAwareOpen)
{
ConfigureBlankContentControl();
}
else
{
CaptureContentControl();
}
}
}
+7 -6
View File
@@ -175,7 +175,7 @@ internal class PopupBuddyLayer : SceneLayer, IPopupBuddyLayer, IShadowAwareLayer
public void Attach()
{
SetupPopupHost();
_buddyDecorator.ConfigureBlankContentControl();
_buddyDecorator.CaptureContentControl();
if (OperatingSystem.IsMacOS())
{
PlatformImpl?.SetTopmost(true);
@@ -211,8 +211,7 @@ internal class PopupBuddyLayer : SceneLayer, IPopupBuddyLayer, IShadowAwareLayer
}
}
public void AttachWithMotion(Action? aboutToStart = null,
Action? completedAction = null)
public void AttachWithMotion(Action? aboutToStart = null, Action? completedAction = null)
{
_buddyDecorator.CaptureContentControl();
_buddyDecorator.Opacity = 0.0d;
@@ -227,13 +226,15 @@ internal class PopupBuddyLayer : SceneLayer, IPopupBuddyLayer, IShadowAwareLayer
motion.Run(_buddyDecorator, null, () =>
{
completedAction?.Invoke();
_buddyDecorator.ConfigureBlankContentControl();
if (_buddyPopup.ConfigureBlankMaskWhenMotionAwareOpen)
{
_buddyDecorator.ConfigureBlankContentControl();
}
NotifyAttachMotionCompleted();
});
}
public void DetachWithMotion(Action? aboutToStart = null,
Action? completedAction = null)
public void DetachWithMotion(Action? aboutToStart = null, Action? completedAction = null)
{
var motion = CloseMotion ?? new ZoomBigOutMotion();
if (MotionDuration != TimeSpan.Zero)