using System.Reactive.Disposables; using AtomUI.Controls.Themes; using AtomUI.Data; using AtomUI.Theme; using AtomUI.Theme.Data; using AtomUI.Theme.Styling; using Avalonia; using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.VisualTree; namespace AtomUI.Controls; public class NavMenuItemClickEventArgs : RoutedEventArgs { public NavMenuItemClickEventArgs(RoutedEvent routedEvent, INavMenuItem navMenuItem) : base(routedEvent) { NavMenuItem = navMenuItem; } public INavMenuItem NavMenuItem { get; } } [PseudoClasses( NavMenuPseudoClass.InlineMode, NavMenuPseudoClass.HorizontalMode, NavMenuPseudoClass.VerticalMode, NavMenuPseudoClass.DarkStyle, NavMenuPseudoClass.LightStyle)] public class NavMenu : NavMenuBase, IResourceBindingManager { #region 公共属性定义 public static readonly StyledProperty ModeProperty = AvaloniaProperty.Register(nameof(Mode), NavMenuMode.Horizontal); public static readonly StyledProperty IsDarkStyleProperty = AvaloniaProperty.Register(nameof(IsDarkStyle), false); public static readonly StyledProperty ActiveBarWidthProperty = AvaloniaProperty.Register(nameof(ActiveBarWidth), 1.0d, coerce: (o, v) => Math.Max(Math.Min(v, 1.0), 0.0)); public static readonly StyledProperty ActiveBarHeightProperty = AvaloniaProperty.Register(nameof(ActiveBarHeight)); public NavMenuMode Mode { get => GetValue(ModeProperty); set => SetValue(ModeProperty, value); } public bool IsDarkStyle { get => GetValue(IsDarkStyleProperty); set => SetValue(IsDarkStyleProperty, value); } public double ActiveBarWidth { get => GetValue(ActiveBarWidthProperty); set => SetValue(ActiveBarWidthProperty, value); } public double ActiveBarHeight { get => GetValue(ActiveBarHeightProperty); set => SetValue(ActiveBarHeightProperty, value); } #endregion #region 公共事件定义 public static readonly RoutedEvent NavMenuItemClickEvent = RoutedEvent.Register( nameof(NavMenuItemClick), RoutingStrategies.Bubble); public event EventHandler? NavMenuItemClick { add => AddHandler(NavMenuItemClickEvent, value); remove => RemoveHandler(NavMenuItemClickEvent, value); } #endregion #region 内部属性定义 internal static readonly StyledProperty HorizontalBorderThicknessProperty = AvaloniaProperty.Register(nameof(HorizontalBorderThickness)); public double HorizontalBorderThickness { get => GetValue(HorizontalBorderThicknessProperty); set => SetValue(HorizontalBorderThicknessProperty, value); } #endregion private static readonly FuncTemplate DefaultPanel = new(() => new StackPanel { Orientation = Orientation.Vertical }); static NavMenu() { ItemsPanelProperty.OverrideDefaultValue(typeof(NavMenu), DefaultPanel); KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue( typeof(NavMenu), KeyboardNavigationMode.Once); AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue(AccessibilityView.Control); AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Menu); AutoScrollToSelectedItemProperty.OverrideDefaultValue(false); } private ItemsPresenter? _menuItemsPresenter; CompositeDisposable? IResourceBindingManager.ResourceBindingsDisposable { get; set; } public NavMenu() { UpdatePseudoClasses(); } public override void Close() { if (!IsOpen) { return; } foreach (var i in ((INavMenu)this).SubItems) { i.Close(); } IsOpen = false; SelectedIndex = -1; RaiseEvent(new RoutedEventArgs { RoutedEvent = ClosedEvent, Source = this, }); } public override void Open() { if (IsOpen) { return; } IsOpen = true; RaiseEvent(new RoutedEventArgs { RoutedEvent = OpenedEvent, Source = this, }); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == IsDarkStyleProperty || change.Property == ModeProperty) { UpdatePseudoClasses(); } if (change.Property == ModeProperty) { ConfigureControlTheme(true); ConfigureItemContainerTheme(true); } if (this.IsAttachedToVisualTree()) { if (change.Property == ModeProperty) { HandleModeChanged(); } } } private void HandleModeChanged() { CloseChildItemsRecursively(); RegenerateContainersRecursively(); SetupMenuItemsPresenter(); SetupInteractionHandler(true); } private void SetupMenuItemsPresenter() { if (_menuItemsPresenter?.Panel is StackPanel stackPanel) { if (Mode == NavMenuMode.Horizontal) { stackPanel.Orientation = Orientation.Horizontal; } else { stackPanel.Orientation = Orientation.Vertical; } } } protected override void PrepareContainerForItemOverride(Control element, object? item, int index) { base.PrepareContainerForItemOverride(element, item, index); // Child menu items should not inherit the menu's ItemContainerTheme as that is specific // for top-level menu items. if ((element as NavMenuItem)?.ItemContainerTheme == ItemContainerTheme) { element.ClearValue(ItemContainerThemeProperty); } if (element is NavMenuItem navMenuItem) { if (Mode == NavMenuMode.Horizontal) { BindUtils.RelayBind(this, ActiveBarHeightProperty, navMenuItem, NavMenuItem.ActiveBarHeightProperty); BindUtils.RelayBind(this, ActiveBarWidthProperty, navMenuItem, NavMenuItem.ActiveBarWidthProperty); } else { BindUtils.RelayBind(this, ItemContainerThemeProperty, navMenuItem, ItemContainerThemeProperty); } BindUtils.RelayBind(this, ModeProperty, navMenuItem, NavMenuItem.ModeProperty); BindUtils.RelayBind(this, IsDarkStyleProperty, navMenuItem, NavMenuItem.IsDarkStyleProperty); BindUtils.RelayBind(this, IsMotionEnabledProperty, navMenuItem, NavMenuItem.IsMotionEnabledProperty); } } private void UpdatePseudoClasses() { PseudoClasses.Set(NavMenuPseudoClass.HorizontalMode, Mode == NavMenuMode.Horizontal); PseudoClasses.Set(NavMenuPseudoClass.VerticalMode, Mode == NavMenuMode.Vertical); PseudoClasses.Set(NavMenuPseudoClass.InlineMode, Mode == NavMenuMode.Inline); PseudoClasses.Set(NavMenuPseudoClass.DarkStyle, IsDarkStyle); PseudoClasses.Set(NavMenuPseudoClass.LightStyle, !IsDarkStyle); } public override void EndInit() { ConfigureControlTheme(false); ConfigureItemContainerTheme(false); base.EndInit(); } protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnAttachedToLogicalTree(e); this.AddResourceBindingDisposable(TokenResourceBinder.CreateTokenBinding(this, HorizontalBorderThicknessProperty, SharedTokenKey.LineWidth, BindingPriority.Template, new RenderScaleAwareDoubleConfigure(this))); SetupInteractionHandler(); } protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnDetachedFromLogicalTree(e); this.DisposeTokenBindings(); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); InteractionHandler?.Attach(this); } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); InteractionHandler?.Detach(this); } 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 ConfigureControlTheme(bool force) { string? resourceKey = null; if (Theme == null || force) { if (Mode == NavMenuMode.Horizontal) { resourceKey = NavMenuThemeConstants.HorizontalNavMenuThemeId; } else { resourceKey = NavMenuThemeConstants.VerticalNavMenuThemeId; } if (Application.Current != null) { if (Application.Current.TryFindResource(resourceKey, out var resource)) { if (resource is ControlTheme theme) { Theme = theme; } } } } } private void ConfigureItemContainerTheme(bool force) { if (ItemContainerTheme is null || force) { string resourceKey; if (Mode == NavMenuMode.Vertical) { resourceKey = NavMenuThemeConstants.VerticalNavMenuItemThemeId; } else if (Mode == NavMenuMode.Inline) { resourceKey = NavMenuThemeConstants.InlineNavMenuItemThemeId; } else { resourceKey = NavMenuThemeConstants.TopLevelHorizontalNavMenuItemThemeId; } if (Application.Current != null) { if (Application.Current.TryFindResource(resourceKey, out var resource)) { if (resource is ControlTheme theme) { ItemContainerTheme = theme; } } } } } internal void ClearSelection() { foreach (var item in Items) { if (item is NavMenuItem navMenuItem) { ClearSelectionRecursively(navMenuItem); } } } internal static void ClearSelectionRecursively(NavMenuItem item, bool skipSelf = false) { if (!skipSelf) { item.IsSelected = false; } foreach (var childItem in item.Items) { if (childItem is NavMenuItem navMenuItem) { ClearSelectionRecursively(navMenuItem); } } } private void CloseChildItemsRecursively() { CloseInlineItems(); foreach (var i in ((INavMenu)this).SubItems) { i.Close(); } } private void RegenerateContainersRecursively() { foreach (var item in Items) { if (item is NavMenuItem navMenuItem) { navMenuItem.RegenerateContainers(); } } } private void CloseInlineItems() { foreach (var item in Items) { if (item is NavMenuItem navMenuItem) { CloseInlineItemRecursively(navMenuItem); } } } private void CloseInlineItemRecursively(NavMenuItem navMenuItem) { foreach (var item in navMenuItem.Items) { if (item is NavMenuItem childNavMenuItem) { CloseInlineItemRecursively(childNavMenuItem); } } navMenuItem.CloseInlineItem(true); navMenuItem.IsSubMenuOpen = false; } internal void RaiseNavMenuItemClick(INavMenuItem navMenuItem) { RaiseEvent(new NavMenuItemClickEventArgs(NavMenuItemClickEvent, navMenuItem)); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); _menuItemsPresenter = e.NameScope.Find(NavMenuThemeConstants.ItemsPresenterPart); SetupMenuItemsPresenter(); HandleModeChanged(); } }