diff --git a/src/AtomUI.Controls/Menu/ContextMenu.cs b/src/AtomUI.Controls/Menu/ContextMenu.cs index 854228bff..0c691ae0a 100644 --- a/src/AtomUI.Controls/Menu/ContextMenu.cs +++ b/src/AtomUI.Controls/Menu/ContextMenu.cs @@ -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("PopupClosed"); + + _popup.Opened += this.CreateEventHandler("PopupOpened"); + _popup.Closed += this.CreateEventHandler("PopupClosed"); - popup.AddClosingEventHandler(this.CreateEventHandler("PopupClosing")!); - popup.KeyUp += this.CreateEventHandler("PopupKeyUp"); + _popup.AddClosingEventHandler(this.CreateEventHandler("PopupClosing")!); + _popup.KeyUp += this.CreateEventHandler("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; + } + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/Menu/Themes/MenuItemTheme.axaml b/src/AtomUI.Controls/Menu/Themes/MenuItemTheme.axaml index d768a2255..4f9893576 100644 --- a/src/AtomUI.Controls/Menu/Themes/MenuItemTheme.axaml +++ b/src/AtomUI.Controls/Menu/Themes/MenuItemTheme.axaml @@ -72,7 +72,7 @@ WindowManagerAddShadowHint="False" IsLightDismissEnabled="False" Placement="RightEdgeAlignedTop" - IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"> + IsMotionAwareOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"> + IsMotionAwareOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"> diff --git a/src/AtomUI.Controls/NavMenu/DefaultNavMenuInteractionHandler.cs b/src/AtomUI.Controls/NavMenu/DefaultNavMenuInteractionHandler.cs index 697318db9..cee558c0b 100644 --- a/src/AtomUI.Controls/NavMenu/DefaultNavMenuInteractionHandler.cs +++ b/src/AtomUI.Controls/NavMenu/DefaultNavMenuInteractionHandler.cs @@ -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; diff --git a/src/AtomUI.Controls/NavMenu/NavMenuBase.cs b/src/AtomUI.Controls/NavMenu/NavMenuBase.cs index f7a5a89f7..754399ec0 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenuBase.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenuBase.cs @@ -23,8 +23,11 @@ public abstract class NavMenuBase : SelectingItemsControl, nameof(IsOpen), o => o.IsOpen); - public static readonly StyledProperty IsMotionEnabledProperty - = MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner(); + public static readonly StyledProperty IsMotionEnabledProperty = + MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner(); + + public static readonly StyledProperty IsAccordionModeProperty = + AvaloniaProperty.Register(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, /// protected internal INavMenuInteractionHandler? InteractionHandler { get; protected set; } + static NavMenuBase() + { + NavMenuItem.SubmenuOpenedEvent.AddClassHandler((x, e) => x.OnSubmenuOpened(e)); + } + public NavMenuBase() { this.RegisterResources(); @@ -151,17 +165,34 @@ public abstract class NavMenuBase : SelectingItemsControl, /// The event args. protected virtual void OnSubmenuOpened(RoutedEventArgs e) { - if (e.Source is NavMenuItem menuItem && menuItem.Parent == this) + if (IsAccordionMode) { - foreach (var child in this.GetLogicalChildren().OfType()) + if (e.Source is NavMenuItem menuItem && menuItem.Parent == this) { - if (child != menuItem && child.IsSubMenuOpen) + foreach (var child in this.GetLogicalChildren().OfType()) + { + 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()) + { + foreach (var child in this.GetLogicalChildren().OfType()) { child.IsSubMenuOpen = false; } } } - - IsOpen = true; } } \ No newline at end of file diff --git a/src/AtomUI.Controls/NavMenu/NavMenuItem.cs b/src/AtomUI.Controls/NavMenu/NavMenuItem.cs index 58141c20d..644f87e78 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenuItem.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenuItem.cs @@ -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); } diff --git a/src/AtomUI.Controls/NavMenu/Themes/NavMenuItemTheme.axaml b/src/AtomUI.Controls/NavMenu/Themes/NavMenuItemTheme.axaml index 4571007f8..a7dae9479 100644 --- a/src/AtomUI.Controls/NavMenu/Themes/NavMenuItemTheme.axaml +++ b/src/AtomUI.Controls/NavMenu/Themes/NavMenuItemTheme.axaml @@ -59,7 +59,7 @@ WindowManagerAddShadowHint="False" IsLightDismissEnabled="False" Placement="RightEdgeAlignedTop" - IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"> + IsMotionAwareOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"> + IsMotionAwareOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"> diff --git a/src/AtomUI.Controls/Popup/Popup.cs b/src/AtomUI.Controls/Popup/Popup.cs index ced837239..ac9b7c0e1 100644 --- a/src/AtomUI.Controls/Popup/Popup.cs +++ b/src/AtomUI.Controls/Popup/Popup.cs @@ -56,6 +56,12 @@ public class Popup : AvaloniaPopup, public static readonly StyledProperty IsWaveSpiritEnabledProperty = WaveSpiritAwareControlProperty.IsWaveSpiritEnabledProperty.AddOwner(); + + public static readonly StyledProperty IsMotionAwareOpenProperty = + AvaloniaProperty.Register(nameof (IsMotionAwareOpen)); + + public static readonly StyledProperty ConfigureBlankMaskWhenMotionAwareOpenProperty = + AvaloniaProperty.Register(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); + } + + /// + /// When the animation is turned on and off, whether to not make a complete mask after the animation ends, including the content + /// + 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()) + { + 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; } -} \ No newline at end of file +} diff --git a/src/AtomUI.Controls/Popup/PopupBuddyDecorator.cs b/src/AtomUI.Controls/Popup/PopupBuddyDecorator.cs index 8c7a7e754..be9afa38a 100644 --- a/src/AtomUI.Controls/Popup/PopupBuddyDecorator.cs +++ b/src/AtomUI.Controls/Popup/PopupBuddyDecorator.cs @@ -106,7 +106,14 @@ internal class PopupBuddyDecorator : SceneMotionActorControl if (sender is ContentPresenter presenter) { ConfigureDecorator(presenter); - ConfigureBlankContentControl(); + if (_popup.ConfigureBlankMaskWhenMotionAwareOpen) + { + ConfigureBlankContentControl(); + } + else + { + CaptureContentControl(); + } } } diff --git a/src/AtomUI.Controls/Popup/PopupBuddyLayer.cs b/src/AtomUI.Controls/Popup/PopupBuddyLayer.cs index 68dbec304..3bc179f12 100644 --- a/src/AtomUI.Controls/Popup/PopupBuddyLayer.cs +++ b/src/AtomUI.Controls/Popup/PopupBuddyLayer.cs @@ -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)