From 9c00a966f45642f68b21f7948c2ff1e960afe22c Mon Sep 17 00:00:00 2001 From: polarboy Date: Mon, 23 Sep 2024 20:55:02 +0800 Subject: [PATCH] Complete the expansion and closing of the inline navigation menu --- .../ShowCase/MenuShowCase.axaml | 21 ++ .../TokenResourceConst.g.cs | 1 + .../NavMenu/BaseNavMenuItemTheme.cs | 203 +++++++++--------- .../DefaultNavMenuInteractionHandler.cs | 94 +++----- src/AtomUI.Controls/NavMenu/INavMenu.cs | 2 +- .../InlineNavMenuInteractionHandler.cs | 86 ++++++++ .../NavMenu/InlineNavMenuItemTheme.cs | 145 ++++++++++++- src/AtomUI.Controls/NavMenu/NavMenu.cs | 55 +++-- src/AtomUI.Controls/NavMenu/NavMenuBase.cs | 28 +-- src/AtomUI.Controls/NavMenu/NavMenuItem.cs | 29 +++ .../NavMenu/NavMenuItemTheme.cs | 4 +- src/AtomUI.Controls/NavMenu/NavMenuToken.cs | 14 +- .../TopLevelHorizontalNavMenuItemTheme.cs | 2 +- .../Utils/Motion/SlideMotionFactory.cs | 50 ++++- 14 files changed, 514 insertions(+), 220 deletions(-) create mode 100644 src/AtomUI.Controls/NavMenu/InlineNavMenuInteractionHandler.cs diff --git a/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml b/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml index f56bdceaa..8b09b25d3 100644 --- a/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml +++ b/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml @@ -229,6 +229,27 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs b/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs index 40cfba1d0..2a409336c 100644 --- a/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs +++ b/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs @@ -405,6 +405,7 @@ namespace AtomUI.Theme.Styling public static readonly TokenResourceKey ActiveBarHeight = new TokenResourceKey("NavMenu.ActiveBarHeight", "AtomUI.Token"); public static readonly TokenResourceKey ItemContentMargin = new TokenResourceKey("NavMenu.ItemContentMargin", "AtomUI.Token"); public static readonly TokenResourceKey ItemContentPadding = new TokenResourceKey("NavMenu.ItemContentPadding", "AtomUI.Token"); + public static readonly TokenResourceKey VerticalItemsPanelSpacing = new TokenResourceKey("NavMenu.VerticalItemsPanelSpacing", "AtomUI.Token"); public static readonly TokenResourceKey ItemMargin = new TokenResourceKey("NavMenu.ItemMargin", "AtomUI.Token"); public static readonly TokenResourceKey HorizontalItemMargin = new TokenResourceKey("NavMenu.HorizontalItemMargin", "AtomUI.Token"); public static readonly TokenResourceKey HorizontalItemHoverBg = new TokenResourceKey("NavMenu.HorizontalItemHoverBg", "AtomUI.Token"); diff --git a/src/AtomUI.Controls/NavMenu/BaseNavMenuItemTheme.cs b/src/AtomUI.Controls/NavMenu/BaseNavMenuItemTheme.cs index 915728ffa..8a873b579 100644 --- a/src/AtomUI.Controls/NavMenu/BaseNavMenuItemTheme.cs +++ b/src/AtomUI.Controls/NavMenu/BaseNavMenuItemTheme.cs @@ -38,109 +38,114 @@ internal class BaseNavMenuItemTheme : BaseControlTheme BuildInstanceStyles(item); // 仅仅为了把 Popup 包进来,没有其他什么作用 var layoutWrapper = new Panel(); - var container = new Border - { - Name = ItemDecoratorPart - }; - - var transitions = new Transitions(); - transitions.Add(AnimationUtils.CreateTransition(Border.BackgroundProperty)); - container.Transitions = transitions; - - var layout = new Grid - { - Name = MainContainerPart, - ColumnDefinitions = - { - new ColumnDefinition(GridLength.Auto) - { - SharedSizeGroup = ThemeConstants.IconPresenterSizeGroup - }, - new ColumnDefinition(GridLength.Star), - new ColumnDefinition(GridLength.Auto) - { - SharedSizeGroup = ThemeConstants.InputGestureTextSizeGroup - }, - new ColumnDefinition(GridLength.Auto) - { - SharedSizeGroup = ThemeConstants.MenuIndicatorIconSizeGroup - } - } - }; - layout.RegisterInNameScope(scope); - - var iconPresenter = new Viewbox - { - Name = ItemIconPresenterPart, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - Stretch = Stretch.Uniform - }; - - Grid.SetColumn(iconPresenter, 0); - iconPresenter.RegisterInNameScope(scope); - CreateTemplateParentBinding(iconPresenter, Viewbox.ChildProperty, NavMenuItem.IconProperty); - TokenResourceBinder.CreateTokenBinding(iconPresenter, Layoutable.MarginProperty, - NavMenuTokenResourceKey.ItemMargin); - TokenResourceBinder.CreateGlobalTokenBinding(iconPresenter, Layoutable.WidthProperty, - NavMenuTokenResourceKey.ItemIconSize); - TokenResourceBinder.CreateGlobalTokenBinding(iconPresenter, Layoutable.HeightProperty, - NavMenuTokenResourceKey.ItemIconSize); - - var itemTextPresenter = new ContentPresenter - { - Name = ItemTextPresenterPart, - HorizontalAlignment = HorizontalAlignment.Stretch, - VerticalAlignment = VerticalAlignment.Center, - RecognizesAccessKey = true, - IsHitTestVisible = false - }; - Grid.SetColumn(itemTextPresenter, 1); - TokenResourceBinder.CreateTokenBinding(itemTextPresenter, Layoutable.MarginProperty, - NavMenuTokenResourceKey.ItemMargin); - CreateTemplateParentBinding(itemTextPresenter, ContentPresenter.ContentProperty, - HeaderedSelectingItemsControl.HeaderProperty); - CreateTemplateParentBinding(itemTextPresenter, ContentPresenter.ContentTemplateProperty, - HeaderedSelectingItemsControl.HeaderTemplateProperty); - - itemTextPresenter.RegisterInNameScope(scope); - - var inputGestureText = new TextBlock - { - Name = InputGestureTextPart, - HorizontalAlignment = HorizontalAlignment.Right, - TextAlignment = TextAlignment.Right, - VerticalAlignment = VerticalAlignment.Center - }; - Grid.SetColumn(inputGestureText, 2); - TokenResourceBinder.CreateTokenBinding(inputGestureText, Layoutable.MarginProperty, - NavMenuTokenResourceKey.ItemMargin); - CreateTemplateParentBinding(inputGestureText, - TextBlock.TextProperty, - NavMenuItem.InputGestureProperty, - BindingMode.Default, - NavMenuItem.KeyGestureConverter); - - inputGestureText.RegisterInNameScope(scope); - - var menuIndicatorIcon = BuildMenuIndicatorIcon(); - Grid.SetColumn(menuIndicatorIcon, 3); - menuIndicatorIcon.RegisterInNameScope(scope); - - layout.Children.Add(iconPresenter); - layout.Children.Add(itemTextPresenter); - layout.Children.Add(inputGestureText); - layout.Children.Add(menuIndicatorIcon); - - BuildExtraItem(layout, scope); - - container.Child = layout; - layoutWrapper.Children.Add(container); + var header = BuildMenuItemContent(scope); + BuildExtraItem(layoutWrapper, scope); + layoutWrapper.Children.Add(header); return layoutWrapper; }); } - protected virtual void BuildExtraItem(Grid containerLayout, INameScope scope) + protected virtual Control BuildMenuItemContent(INameScope scope) + { + var headerFrame = new Border + { + Name = ItemDecoratorPart + }; + + var transitions = new Transitions(); + transitions.Add(AnimationUtils.CreateTransition(Border.BackgroundProperty)); + headerFrame.Transitions = transitions; + + var layout = new Grid + { + Name = MainContainerPart, + ColumnDefinitions = + { + new ColumnDefinition(GridLength.Auto) + { + SharedSizeGroup = ThemeConstants.IconPresenterSizeGroup + }, + new ColumnDefinition(GridLength.Star), + new ColumnDefinition(GridLength.Auto) + { + SharedSizeGroup = ThemeConstants.InputGestureTextSizeGroup + }, + new ColumnDefinition(GridLength.Auto) + { + SharedSizeGroup = ThemeConstants.MenuIndicatorIconSizeGroup + } + } + }; + layout.RegisterInNameScope(scope); + + var iconPresenter = new Viewbox + { + Name = ItemIconPresenterPart, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Stretch = Stretch.Uniform + }; + + Grid.SetColumn(iconPresenter, 0); + iconPresenter.RegisterInNameScope(scope); + CreateTemplateParentBinding(iconPresenter, Viewbox.ChildProperty, NavMenuItem.IconProperty); + TokenResourceBinder.CreateTokenBinding(iconPresenter, Layoutable.MarginProperty, + NavMenuTokenResourceKey.ItemMargin); + TokenResourceBinder.CreateGlobalTokenBinding(iconPresenter, Layoutable.WidthProperty, + NavMenuTokenResourceKey.ItemIconSize); + TokenResourceBinder.CreateGlobalTokenBinding(iconPresenter, Layoutable.HeightProperty, + NavMenuTokenResourceKey.ItemIconSize); + + var itemTextPresenter = new ContentPresenter + { + Name = ItemTextPresenterPart, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Center, + RecognizesAccessKey = true, + IsHitTestVisible = false + }; + Grid.SetColumn(itemTextPresenter, 1); + TokenResourceBinder.CreateTokenBinding(itemTextPresenter, Layoutable.MarginProperty, + NavMenuTokenResourceKey.ItemMargin); + CreateTemplateParentBinding(itemTextPresenter, ContentPresenter.ContentProperty, + HeaderedSelectingItemsControl.HeaderProperty); + CreateTemplateParentBinding(itemTextPresenter, ContentPresenter.ContentTemplateProperty, + HeaderedSelectingItemsControl.HeaderTemplateProperty); + + itemTextPresenter.RegisterInNameScope(scope); + + var inputGestureText = new TextBlock + { + Name = InputGestureTextPart, + HorizontalAlignment = HorizontalAlignment.Right, + TextAlignment = TextAlignment.Right, + VerticalAlignment = VerticalAlignment.Center + }; + Grid.SetColumn(inputGestureText, 2); + TokenResourceBinder.CreateTokenBinding(inputGestureText, Layoutable.MarginProperty, + NavMenuTokenResourceKey.ItemMargin); + CreateTemplateParentBinding(inputGestureText, + TextBlock.TextProperty, + NavMenuItem.InputGestureProperty, + BindingMode.Default, + NavMenuItem.KeyGestureConverter); + + inputGestureText.RegisterInNameScope(scope); + + var menuIndicatorIcon = BuildMenuIndicatorIcon(); + Grid.SetColumn(menuIndicatorIcon, 3); + menuIndicatorIcon.RegisterInNameScope(scope); + + layout.Children.Add(iconPresenter); + layout.Children.Add(itemTextPresenter); + layout.Children.Add(inputGestureText); + layout.Children.Add(menuIndicatorIcon); + + headerFrame.Child = layout; + return headerFrame; + } + + protected virtual void BuildExtraItem(Panel layout, INameScope scope) { } diff --git a/src/AtomUI.Controls/NavMenu/DefaultNavMenuInteractionHandler.cs b/src/AtomUI.Controls/NavMenu/DefaultNavMenuInteractionHandler.cs index 225c0df41..80dbdf126 100644 --- a/src/AtomUI.Controls/NavMenu/DefaultNavMenuInteractionHandler.cs +++ b/src/AtomUI.Controls/NavMenu/DefaultNavMenuInteractionHandler.cs @@ -11,25 +11,22 @@ using Avalonia.VisualTree; namespace AtomUI.Controls; -public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler +internal class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler { - private readonly bool _isContextMenu; private IDisposable? _inputManagerSubscription; private IRenderRoot? _root; - public DefaultNavMenuInteractionHandler(bool isContextMenu) - : this(isContextMenu, AvaloniaLocator.Current.GetService(), DefaultDelayRun) + public DefaultNavMenuInteractionHandler() + : this(AvaloniaLocator.Current.GetService(), DefaultDelayRun) { } public DefaultNavMenuInteractionHandler( - bool isContextMenu, IInputManager? inputManager, Action delayRun) { delayRun = delayRun ?? throw new ArgumentNullException(nameof(delayRun)); - - _isContextMenu = isContextMenu; + InputManager = inputManager; DelayRun = delayRun; } @@ -45,7 +42,7 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler public static TimeSpan MenuShowDelay { get; set; } = TimeSpan.FromMilliseconds(400); - protected internal virtual void GotFocus(object? sender, GotFocusEventArgs e) + protected virtual void GotFocus(object? sender, GotFocusEventArgs e) { var item = GetMenuItemCore(e.Source as Control); @@ -55,7 +52,7 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler } } - protected internal virtual void LostFocus(object? sender, RoutedEventArgs e) + protected virtual void LostFocus(object? sender, RoutedEventArgs e) { var item = GetMenuItemCore(e.Source as Control); @@ -65,36 +62,14 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler } } - protected internal virtual void KeyDown(object? sender, KeyEventArgs e) + protected virtual void KeyDown(object? sender, KeyEventArgs e) { KeyDown(GetMenuItemCore(e.Source as Control), e); } - protected internal virtual void AccessKeyPressed(object? sender, RoutedEventArgs e) + protected virtual void PointerEntered(object? sender, RoutedEventArgs e) { var item = GetMenuItemCore(e.Source as Control); - - if (item == null) - { - return; - } - - if (item.HasSubMenu && item.IsEffectivelyEnabled) - { - Open(item, true); - } - else - { - Click(item); - } - - e.Handled = true; - } - - protected internal virtual void PointerEntered(object? sender, RoutedEventArgs e) - { - var item = GetMenuItemCore(e.Source as Control); - if (item?.Parent == null) { return; @@ -136,7 +111,7 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler } } - protected internal virtual void PointerMoved(object? sender, PointerEventArgs e) + protected virtual void PointerMoved(object? sender, PointerEventArgs e) { // HACK: #8179 needs to be addressed to correctly implement it in the PointerPressed method. var item = GetMenuItemCore(e.Source as Control) as NavMenuItem; @@ -157,7 +132,7 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler } } - protected internal virtual void PointerExited(object? sender, RoutedEventArgs e) + protected virtual void PointerExited(object? sender, RoutedEventArgs e) { var item = GetMenuItemCore(e.Source as Control); @@ -192,7 +167,7 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler } } - protected internal virtual void PointerPressed(object? sender, PointerPressedEventArgs e) + protected virtual void PointerPressed(object? sender, PointerPressedEventArgs e) { var item = GetMenuItemCore(e.Source as Control); @@ -211,11 +186,6 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler } else { - // if (item.IsTopLevel && item.Parent is IMainMenu mainMenu) - // { - // mainNavMenu.Open(); - // } - Open(item, false); } @@ -223,7 +193,7 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler } } - protected internal virtual void PointerReleased(object? sender, PointerReleasedEventArgs e) + protected virtual void PointerReleased(object? sender, PointerReleasedEventArgs e) { var item = GetMenuItemCore(e.Source as Control); @@ -234,7 +204,7 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler } } - protected internal virtual void MenuOpened(object? sender, RoutedEventArgs e) + protected virtual void MenuOpened(object? sender, RoutedEventArgs e) { if (e.Source is NavMenu) { @@ -242,7 +212,7 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler } } - protected internal virtual void RawInput(RawInputEventArgs e) + protected virtual void RawInput(RawInputEventArgs e) { var mouse = e as RawPointerEventArgs; @@ -252,7 +222,7 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler } } - protected internal virtual void RootPointerPressed(object? sender, PointerPressedEventArgs e) + protected virtual void RootPointerPressed(object? sender, PointerPressedEventArgs e) { if (NavMenu?.IsOpen == true) { @@ -263,7 +233,7 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler } } - protected internal virtual void WindowDeactivated(object? sender, EventArgs e) + protected virtual void WindowDeactivated(object? sender, EventArgs e) { NavMenu?.Close(); } @@ -283,10 +253,9 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler NavMenu.KeyDown += KeyDown; NavMenu.PointerPressed += PointerPressed; NavMenu.PointerReleased += PointerReleased; - // NavMenu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed); - NavMenu.AddHandler(MenuBase.OpenedEvent, MenuOpened); - NavMenu.AddHandler(MenuItem.PointerEnteredItemEvent, PointerEntered); - NavMenu.AddHandler(MenuItem.PointerExitedItemEvent, PointerExited); + NavMenu.AddHandler(NavMenuBase.OpenedEvent, MenuOpened); + NavMenu.AddHandler(NavMenuItem.PointerEnteredItemEvent, PointerEntered); + NavMenu.AddHandler(NavMenuItem.PointerExitedItemEvent, PointerExited); NavMenu.AddHandler(InputElement.PointerMovedEvent, PointerMoved); _root = NavMenu.VisualRoot; @@ -302,7 +271,9 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler } if (_root is TopLevel tl && tl.PlatformImpl is ITopLevelImpl pimpl) + { pimpl.LostFocus += TopLevelLostPlatformFocus; + } _inputManagerSubscription = InputManager?.Process.Subscribe(RawInput); } @@ -319,10 +290,9 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler NavMenu.KeyDown -= KeyDown; NavMenu.PointerPressed -= PointerPressed; NavMenu.PointerReleased -= PointerReleased; - // NavMenu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed); - NavMenu.RemoveHandler(MenuBase.OpenedEvent, MenuOpened); - NavMenu.RemoveHandler(MenuItem.PointerEnteredItemEvent, PointerEntered); - NavMenu.RemoveHandler(MenuItem.PointerExitedItemEvent, PointerExited); + NavMenu.RemoveHandler(NavMenuBase.OpenedEvent, MenuOpened); + NavMenu.RemoveHandler(NavMenuItem.PointerEnteredItemEvent, PointerEntered); + NavMenu.RemoveHandler(NavMenuItem.PointerExitedItemEvent, PointerExited); NavMenu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved); if (_root is InputElement inputRoot) @@ -483,14 +453,7 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler if (direction?.IsDirectional() == true) { - if (item == null && _isContextMenu) - { - if (NavMenu!.MoveSelection(direction.Value, true) == true) - { - e.Handled = true; - } - } - else if (item?.Parent?.MoveSelection(direction.Value, true) == true) + if (item?.Parent?.MoveSelection(direction.Value, true) == true) { // If the the parent is an IMenu which successfully moved its selection, // and the current navMenu is open then close the current navMenu and open the @@ -557,9 +520,15 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler while (true) { if (item == null) + { return null; + } + if (item is INavMenuItem menuItem) + { return menuItem; + } + item = item.Parent; } } @@ -573,5 +542,4 @@ public class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler { DispatcherTimer.RunOnce(action, timeSpan); } - } \ No newline at end of file diff --git a/src/AtomUI.Controls/NavMenu/INavMenu.cs b/src/AtomUI.Controls/NavMenu/INavMenu.cs index 2f3b1565a..ce18bb6a7 100644 --- a/src/AtomUI.Controls/NavMenu/INavMenu.cs +++ b/src/AtomUI.Controls/NavMenu/INavMenu.cs @@ -9,7 +9,7 @@ internal interface INavMenu : INavMenuElement, IInputElement /// /// Gets the menu interaction handler. /// - INavMenuInteractionHandler InteractionHandler { get; } + INavMenuInteractionHandler? InteractionHandler { get; } /// /// Gets a value indicating whether the menu is open. diff --git a/src/AtomUI.Controls/NavMenu/InlineNavMenuInteractionHandler.cs b/src/AtomUI.Controls/NavMenu/InlineNavMenuInteractionHandler.cs new file mode 100644 index 000000000..8bd814e92 --- /dev/null +++ b/src/AtomUI.Controls/NavMenu/InlineNavMenuInteractionHandler.cs @@ -0,0 +1,86 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; + +namespace AtomUI.Controls; + +internal class InlineNavMenuInteractionHandler : INavMenuInteractionHandler +{ + internal INavMenu? NavMenu { get; private set; } + + public void Attach(NavMenuBase navMenu) => AttachCore(navMenu); + public void Detach(NavMenuBase navMenu) => DetachCore(navMenu); + + internal void AttachCore(INavMenu navMenu) + { + if (NavMenu != null) + { + throw new NotSupportedException("InlineNavMenuInteractionHandler is already attached."); + } + NavMenu = navMenu; + NavMenu.PointerPressed += PointerPressed; + } + + internal void DetachCore(INavMenu navMenu) + { + if (NavMenu != navMenu) + { + throw new NotSupportedException("InlineNavMenuInteractionHandler is not attached to the navMenu."); + } + NavMenu.PointerPressed -= PointerPressed; + NavMenu = null; + } + + protected virtual void PointerPressed(object? sender, PointerPressedEventArgs e) + { + var item = GetMenuItemCore(e.Source as Control); + + if (sender is Visual visual && + e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) + { + if (item?.HasSubMenu == true) + { + if (item.IsSubMenuOpen) + { + item.Close(); + } + else + { + Open(item); + } + } + else + { + if (NavMenu is NavMenu navMenu) + { + navMenu.UpdateSelectionFromItem(item); + } + } + + e.Handled = true; + } + } + + internal void Open(INavMenuItem item) + { + item.Open(); + } + + internal static INavMenuItem? GetMenuItemCore(StyledElement? item) + { + while (true) + { + if (item == null) + { + return null; + } + + if (item is INavMenuItem menuItem) + { + return menuItem; + } + + item = item.Parent; + } + } +} \ No newline at end of file diff --git a/src/AtomUI.Controls/NavMenu/InlineNavMenuItemTheme.cs b/src/AtomUI.Controls/NavMenu/InlineNavMenuItemTheme.cs index 6b4884c2e..d344e141b 100644 --- a/src/AtomUI.Controls/NavMenu/InlineNavMenuItemTheme.cs +++ b/src/AtomUI.Controls/NavMenu/InlineNavMenuItemTheme.cs @@ -1,13 +1,29 @@ -using AtomUI.Theme.Styling; +using AtomUI.Controls.Utils; +using AtomUI.Theme.Styling; +using AtomUI.Theme.Utils; +using AtomUI.Utils; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Media.Transformation; +using Avalonia.Styling; namespace AtomUI.Controls; [ControlThemeProvider] -internal class InlineNavMenuItemTheme : VerticalNavMenuItemTheme +internal class InlineNavMenuItemTheme : BaseNavMenuItemTheme { public new const string ID = "InlineNavMenuItem"; public const string MenuIndicatorIconLayoutPart = "PART_MenuIndicatorIconLayout"; + public const string ChildItemsPresenterPart = "PART_ChildItemsPresenter"; + public const string ChildItemsLayoutTransformPart = "PART_ChildItemsLayoutTransform"; public InlineNavMenuItemTheme() : base(typeof(NavMenuItem)) { @@ -21,11 +37,128 @@ internal class InlineNavMenuItemTheme : VerticalNavMenuItemTheme protected override Control BuildMenuIndicatorIcon() { var indicatorIcon = base.BuildMenuIndicatorIcon(); - var layoutTransformControl = new LayoutTransformControl() + var menuIndicatorIconPresenter = new ContentPresenter() { - Name = MenuIndicatorIconLayoutPart + Name = MenuIndicatorIconLayoutPart, + Transitions = new Transitions() + { + AnimationUtils.CreateTransition(ContentPresenter.RenderTransformProperty) + } }; - layoutTransformControl.Child = indicatorIcon; - return layoutTransformControl; + menuIndicatorIconPresenter.Content = indicatorIcon; + return menuIndicatorIconPresenter; } + + protected override Control BuildMenuItemContent(INameScope scope) + { + var rootLayout = new StackPanel() + { + Orientation = Orientation.Vertical + }; + var headerContent = base.BuildMenuItemContent(scope); + + TokenResourceBinder.CreateTokenBinding(headerContent, Control.MarginProperty, NavMenuTokenResourceKey.VerticalItemsPanelSpacing, BindingPriority.Template, + (v) => + { + if (v is double dval) + { + return new Thickness(0, 0, 0, dval); + } + + return new Thickness(); + }); + var childItemsLayoutTransform = new LayoutTransformControl() + { + Name = ChildItemsLayoutTransformPart, + }; + + var itemsPresenter = new ItemsPresenter + { + Name = ChildItemsPresenterPart, + Focusable = false, + }; + itemsPresenter.RegisterInNameScope(scope); + CreateTemplateParentBinding(itemsPresenter, ItemsPresenter.ItemsPanelProperty, NavMenuItem.ItemsPanelProperty); + + childItemsLayoutTransform.Child = itemsPresenter; + CreateTemplateParentBinding(itemsPresenter, Visual.IsVisibleProperty, NavMenuItem.IsSubMenuOpenProperty); + + rootLayout.Children.Add(headerContent); + rootLayout.Children.Add(childItemsLayoutTransform); + return rootLayout; + } + + protected override void BuildStyles() + { + base.BuildStyles(); + var childItemsPanelStyle = new Style(selector => selector.Nesting().Template().Name(ChildItemsPresenterPart).Child().OfType()); + childItemsPanelStyle.Add(StackPanel.SpacingProperty, NavMenuTokenResourceKey.VerticalItemsPanelSpacing); + Add(childItemsPanelStyle); + + BuildMenuIndicatorStyle(); + } + + private void BuildMenuIndicatorStyle() + { + { + var menuIndicatorStyle = new Style(selector => selector.Nesting().Template().Name(MenuIndicatorIconLayoutPart)); + var transformOptions = new TransformOperations.Builder(1); + transformOptions.AppendRotate(MathUtils.Deg2Rad(90)); + menuIndicatorStyle.Add(ContentPresenter.RenderTransformProperty, transformOptions.Build()); + menuIndicatorStyle.Add(Visual.IsVisibleProperty, true); + Add(menuIndicatorStyle); + } + var openSubMenuStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Open)); + { + var menuIndicatorStyle = new Style(selector => selector.Nesting().Template().Name(MenuIndicatorIconLayoutPart)); + var transformOptions = new TransformOperations.Builder(1); + transformOptions.AppendRotate(MathUtils.Deg2Rad(-90)); + menuIndicatorStyle.Add(ContentPresenter.RenderTransformProperty, transformOptions.Build()); + openSubMenuStyle.Add(menuIndicatorStyle); + } + Add(openSubMenuStyle); + var emptySubMenuStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Empty)); + { + var menuIndicatorStyle = new Style(selector => selector.Nesting().Template().Name(MenuIndicatorIconLayoutPart)); + menuIndicatorStyle.Add(Visual.IsVisibleProperty, false); + emptySubMenuStyle.Add(menuIndicatorStyle); + } + Add(emptySubMenuStyle); + } + + // private void BuildAnimationStyle() + // { + // var closeSubMenuStyle = new Style(selector => selector.Nesting().Not(selector.Nesting().Class(StdPseudoClass.Open))); + // { + // var layoutTransformStyle = + // new Style(selector => selector.Nesting().Template().Name(ChildItemsLayoutTransformPart)); + // var slideDownInMotionConfig = MotionFactory.BuildSlideUpOutMotion(TimeSpan.FromMilliseconds(300), new QuadraticEaseIn(), + // FillMode.Forward); + // foreach (var animation in slideDownInMotionConfig.Animations) + // { + // layoutTransformStyle.Animations.Add(animation); + // } + // layoutTransformStyle.Add(LayoutTransformControl.RenderTransformOriginProperty, slideDownInMotionConfig.RenderTransformOrigin); + // layoutTransformStyle.Add(LayoutTransformControl.IsVisibleProperty, false); + // closeSubMenuStyle.Add(layoutTransformStyle); + // } + // Add(closeSubMenuStyle); + // + // var openSubMenuStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Open)); + // { + // var layoutTransformStyle = + // new Style(selector => selector.Nesting().Template().Name(ChildItemsLayoutTransformPart)); + // var slideDownInMotionConfig = MotionFactory.BuildSlideUpInMotion(TimeSpan.FromMilliseconds(300), new QuadraticEaseIn(), + // FillMode.Forward); + // foreach (var animation in slideDownInMotionConfig.Animations) + // { + // layoutTransformStyle.Animations.Add(animation); + // } + // + // layoutTransformStyle.Add(LayoutTransformControl.RenderTransformOriginProperty, slideDownInMotionConfig.RenderTransformOrigin); + // layoutTransformStyle.Add(LayoutTransformControl.IsVisibleProperty, true); + // openSubMenuStyle.Add(layoutTransformStyle); + // } + // Add(openSubMenuStyle); + // } } \ No newline at end of file diff --git a/src/AtomUI.Controls/NavMenu/NavMenu.cs b/src/AtomUI.Controls/NavMenu/NavMenu.cs index 236613aea..0fcbf6f2d 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenu.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenu.cs @@ -101,16 +101,6 @@ public class NavMenu : NavMenuBase UpdatePseudoClasses(); } - /// - /// Initializes a new instance of the class. - /// - /// The menu interaction handler. - public NavMenu(INavMenuInteractionHandler interactionHandler) - : base(interactionHandler) - { - UpdatePseudoClasses(); - } - public override void Close() { if (!IsOpen) @@ -158,6 +148,7 @@ public class NavMenu : NavMenuBase if (change.Property == ModeProperty) { SetupItemContainerTheme(true); + SetupInteractionHandler(true); } UpdatePseudoClasses(); } @@ -173,10 +164,14 @@ public class NavMenu : NavMenuBase element.ClearValue(ItemContainerThemeProperty); } - if (element is NavMenuItem navMenuItem && Mode == NavMenuMode.Horizontal) + if (element is NavMenuItem navMenuItem) { - BindUtils.RelayBind(this, ActiveBarHeightProperty, navMenuItem, NavMenuItem.ActiveBarHeightProperty); - BindUtils.RelayBind(this, ActiveBarWidthProperty, navMenuItem, NavMenuItem.ActiveBarWidthProperty); + if (Mode == NavMenuMode.Horizontal) + { + BindUtils.RelayBind(this, ActiveBarHeightProperty, navMenuItem, NavMenuItem.ActiveBarHeightProperty); + BindUtils.RelayBind(this, ActiveBarWidthProperty, navMenuItem, NavMenuItem.ActiveBarWidthProperty); + } + BindUtils.RelayBind(this, ModeProperty, navMenuItem, NavMenuItem.ModeProperty); } } @@ -193,16 +188,23 @@ public class NavMenu : NavMenuBase { base.OnAttachedToLogicalTree(e); SetupItemContainerTheme(); + SetupInteractionHandler(); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - + InteractionHandler?.Attach(this); TokenResourceBinder.CreateGlobalTokenBinding(this, HorizontalBorderThicknessProperty, GlobalTokenResourceKey.LineWidth, BindingPriority.Template, new RenderScaleAwareDoubleConfigure(this)); } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + InteractionHandler?.Detach(this); + } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -211,6 +213,26 @@ public class NavMenu : NavMenuBase TokenResourceBinder.CreateTokenBinding(this, ActiveBarHeightProperty, NavMenuTokenResourceKey.ActiveBarHeight); } + private void SetupInteractionHandler(bool needMount = false) + { + if (needMount) + { + InteractionHandler?.Detach(this); + } + if (Mode == NavMenuMode.Inline) + { + InteractionHandler = new InlineNavMenuInteractionHandler(); + } + else + { + InteractionHandler = new DefaultNavMenuInteractionHandler(); + } + if (needMount) + { + InteractionHandler?.Attach(this); + } + } + private void SetupItemContainerTheme(bool force = false) { if (ItemContainerTheme is null || force) @@ -231,4 +253,9 @@ public class NavMenu : NavMenuBase TokenResourceBinder.CreateGlobalResourceBinding(this, ItemContainerThemeProperty, resourceKey); } } + + internal void UpdateSelectionFromItem(INavMenuItem? item) + { + UpdateSelectionFromEventSource(item); + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/NavMenu/NavMenuBase.cs b/src/AtomUI.Controls/NavMenu/NavMenuBase.cs index 60c3624ff..9d9676160 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenuBase.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenuBase.cs @@ -67,7 +67,7 @@ public abstract class NavMenuBase : SelectingItemsControl, IFocusScope, INavMenu #endregion - INavMenuInteractionHandler INavMenu.InteractionHandler => InteractionHandler; + INavMenuInteractionHandler? INavMenu.InteractionHandler => InteractionHandler; IRenderRoot? INavMenu.VisualRoot => VisualRoot; @@ -87,17 +87,7 @@ public abstract class NavMenuBase : SelectingItemsControl, IFocusScope, INavMenu /// /// Gets the interaction handler for the menu. /// - protected internal INavMenuInteractionHandler InteractionHandler { get; } - - protected NavMenuBase() - { - InteractionHandler = new DefaultNavMenuInteractionHandler(false); - } - - protected NavMenuBase(INavMenuInteractionHandler interactionHandler) - { - InteractionHandler = interactionHandler ?? throw new ArgumentNullException(nameof(interactionHandler)); - } + protected internal INavMenuInteractionHandler? InteractionHandler { get; protected set; } /// /// Closes the menu. @@ -135,20 +125,6 @@ public abstract class NavMenuBase : SelectingItemsControl, IFocusScope, INavMenu // Don't handle here: let the interaction handler handle it. } - /// - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - InteractionHandler.Attach(this); - } - - /// - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - InteractionHandler.Detach(this); - } - /// /// Called when a submenu opens somewhere in the menu. /// diff --git a/src/AtomUI.Controls/NavMenu/NavMenuItem.cs b/src/AtomUI.Controls/NavMenu/NavMenuItem.cs index f50da9228..ca5d791a7 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenuItem.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenuItem.cs @@ -236,6 +236,11 @@ public class NavMenuItem : HeaderedSelectingItemsControl, o => o.PopupMinWidth, (o, v) => o.PopupMinWidth = v); + internal static readonly DirectProperty ModeProperty = + AvaloniaProperty.RegisterDirect(nameof(Mode), + o => o.Mode, + (o, v) => o.Mode = v); + public double ActiveBarWidth { get => GetValue(ActiveBarWidthProperty); @@ -268,6 +273,13 @@ public class NavMenuItem : HeaderedSelectingItemsControl, get => _popupMinWidth; set => SetAndRaise(PopupMinWidthProperty, ref _popupMinWidth, value); } + + private NavMenuMode _mode; + public NavMenuMode Mode + { + get => _mode; + set => SetAndRaise(ModeProperty, ref _mode, value); + } #endregion @@ -371,6 +383,7 @@ public class NavMenuItem : HeaderedSelectingItemsControl, private KeyGesture? _hotkey; private bool _isEmbeddedInMenu; private Border? _horizontalFrame; + private IDisposable? _itemContainerThemeDisposable; static NavMenuItem() { @@ -721,6 +734,9 @@ public class NavMenuItem : HeaderedSelectingItemsControl, else if (change.Property == IconProperty) { SetupItemIcon(); + } else if (change.Property == ModeProperty) + { + SetupItemContainerTheme(true); } if (change.Property == BoundsProperty || @@ -728,6 +744,7 @@ public class NavMenuItem : HeaderedSelectingItemsControl, { SetupEffectivePopupMinWidth(); } + } private void SetupItemIcon() @@ -918,4 +935,16 @@ public class NavMenuItem : HeaderedSelectingItemsControl, return true; } + + private void SetupItemContainerTheme(bool force = false) + { + if (ItemContainerTheme is null || force) + { + _itemContainerThemeDisposable?.Dispose(); + if (Mode == NavMenuMode.Inline) + { + _itemContainerThemeDisposable = TokenResourceBinder.CreateGlobalResourceBinding(this, ItemContainerThemeProperty, InlineNavMenuItemTheme.ID); + } + } + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/NavMenu/NavMenuItemTheme.cs b/src/AtomUI.Controls/NavMenu/NavMenuItemTheme.cs index 8da158eb4..fb905bf0b 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenuItemTheme.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenuItemTheme.cs @@ -22,11 +22,11 @@ internal class NavMenuItemTheme : BaseNavMenuItemTheme { } - protected override void BuildExtraItem(Grid containerLayout, INameScope scope) + protected override void BuildExtraItem(Panel layout, INameScope scope) { var popup = CreateMenuPopup(); popup.RegisterInNameScope(scope); - containerLayout.Children.Add(popup); + layout.Children.Add(popup); } protected Popup CreateMenuPopup() diff --git a/src/AtomUI.Controls/NavMenu/NavMenuToken.cs b/src/AtomUI.Controls/NavMenu/NavMenuToken.cs index 707621fe7..51c48626d 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenuToken.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenuToken.cs @@ -185,6 +185,11 @@ internal class NavMenuToken : AbstractControlDesignToken /// public Thickness ItemContentPadding { get; set; } + /// + /// 垂直面板的元素间距 + /// + public double VerticalItemsPanelSpacing { get; set; } + /// /// 菜单项内部元素边距 /// @@ -436,9 +441,10 @@ internal class NavMenuToken : AbstractControlDesignToken MenuPopupMaxHeight = ItemHeight * 30; TopLevelItemPopupMarginToAnchor = _globalToken.MarginXXS; - MenuPopupBg = _globalToken.ColorBgElevated; - MenuPopupBorderRadius = _globalToken.BorderRadiusLG; - MenuPopupContentPadding = new Thickness(_globalToken.PaddingXXS, MenuPopupBorderRadius.TopLeft / 2); - MenuPopupBoxShadows = _globalToken.BoxShadowsSecondary; + MenuPopupBg = _globalToken.ColorBgElevated; + MenuPopupBorderRadius = _globalToken.BorderRadiusLG; + MenuPopupContentPadding = new Thickness(_globalToken.PaddingXXS, MenuPopupBorderRadius.TopLeft / 2); + MenuPopupBoxShadows = _globalToken.BoxShadowsSecondary; + VerticalItemsPanelSpacing = _globalToken.MarginXXS; } } \ No newline at end of file diff --git a/src/AtomUI.Controls/NavMenu/TopLevelHorizontalNavMenuItemTheme.cs b/src/AtomUI.Controls/NavMenu/TopLevelHorizontalNavMenuItemTheme.cs index 634ab1f7b..98df13e49 100644 --- a/src/AtomUI.Controls/NavMenu/TopLevelHorizontalNavMenuItemTheme.cs +++ b/src/AtomUI.Controls/NavMenu/TopLevelHorizontalNavMenuItemTheme.cs @@ -175,7 +175,7 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme TokenResourceBinder.CreateTokenBinding(popup, Popup.MaskShadowsProperty, MenuTokenResourceKey.MenuPopupBoxShadows); - CreateTemplateParentBinding(popup, Avalonia.Controls.Primitives.Popup.IsOpenProperty, + CreateTemplateParentBinding(popup, Popup.IsOpenProperty, NavMenuItem.IsSubMenuOpenProperty, BindingMode.TwoWay); return popup; diff --git a/src/AtomUI.Controls/Utils/Motion/SlideMotionFactory.cs b/src/AtomUI.Controls/Utils/Motion/SlideMotionFactory.cs index 99e0235f1..87884c6c1 100644 --- a/src/AtomUI.Controls/Utils/Motion/SlideMotionFactory.cs +++ b/src/AtomUI.Controls/Utils/Motion/SlideMotionFactory.cs @@ -36,11 +36,32 @@ public static partial class MotionFactory var scaleYSetter = new Setter { Property = ScaleTransform.ScaleYProperty, - Value = 0.8 + Value = 0.1 }; startFrame.Setters.Add(scaleYSetter); } animation.Children.Add(startFrame); + + var middleFrame = new KeyFrame + { + Cue = new Cue(0.9) + }; + { + var opacitySetter = new Setter + { + Property = Visual.OpacityProperty, + Value = 0.2 + }; + middleFrame.Setters.Add(opacitySetter); + + var scaleYSetter = new Setter + { + Property = ScaleTransform.ScaleYProperty, + Value = 0.9 + }; + middleFrame.Setters.Add(scaleYSetter); + } + animation.Children.Add(middleFrame); var endFrame = new KeyFrame { @@ -100,6 +121,27 @@ public static partial class MotionFactory startFrame.Setters.Add(scaleYSetter); } animation.Children.Add(startFrame); + + var middleFrame = new KeyFrame + { + Cue = new Cue(0.9) + }; + { + var opacitySetter = new Setter + { + Property = Visual.OpacityProperty, + Value = 0.2 + }; + middleFrame.Setters.Add(opacitySetter); + + var scaleYSetter = new Setter + { + Property = ScaleTransform.ScaleYProperty, + Value = 0.9 + }; + middleFrame.Setters.Add(scaleYSetter); + } + animation.Children.Add(middleFrame); var endFrame = new KeyFrame { @@ -115,7 +157,7 @@ public static partial class MotionFactory var scaleYSetter = new Setter { Property = ScaleTransform.ScaleYProperty, - Value = 0.8 + Value = 0.0 }; endFrame.Setters.Add(scaleYSetter); } @@ -154,7 +196,7 @@ public static partial class MotionFactory var scaleYSetter = new Setter { Property = ScaleTransform.ScaleYProperty, - Value = 0.8 + Value = 0.1 }; startFrame.Setters.Add(scaleYSetter); } @@ -168,7 +210,7 @@ public static partial class MotionFactory var opacitySetter = new Setter { Property = Visual.OpacityProperty, - Value = 1.0 + Value = 0.2 }; endFrame.Setters.Add(opacitySetter); var scaleYSetter = new Setter