mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-22 00:46:48 +08:00
456 lines
13 KiB
C#
456 lines
13 KiB
C#
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<NavMenuMode> ModeProperty =
|
|
AvaloniaProperty.Register<NavMenu, NavMenuMode>(nameof(Mode), NavMenuMode.Horizontal);
|
|
|
|
public static readonly StyledProperty<bool> IsDarkStyleProperty =
|
|
AvaloniaProperty.Register<NavMenu, bool>(nameof(IsDarkStyle), false);
|
|
|
|
public static readonly StyledProperty<double> ActiveBarWidthProperty =
|
|
AvaloniaProperty.Register<NavMenu, double>(nameof(ActiveBarWidth), 1.0d,
|
|
coerce: (o, v) => Math.Max(Math.Min(v, 1.0), 0.0));
|
|
|
|
public static readonly StyledProperty<double> ActiveBarHeightProperty =
|
|
AvaloniaProperty.Register<NavMenu, double>(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<NavMenuItemClickEventArgs> NavMenuItemClickEvent =
|
|
RoutedEvent.Register<NavMenu, NavMenuItemClickEventArgs>(
|
|
nameof(NavMenuItemClick),
|
|
RoutingStrategies.Bubble);
|
|
|
|
public event EventHandler<NavMenuItemClickEventArgs>? NavMenuItemClick
|
|
{
|
|
add => AddHandler(NavMenuItemClickEvent, value);
|
|
remove => RemoveHandler(NavMenuItemClickEvent, value);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 内部属性定义
|
|
|
|
internal static readonly StyledProperty<double> HorizontalBorderThicknessProperty =
|
|
AvaloniaProperty.Register<NavMenuItem, double>(nameof(HorizontalBorderThickness));
|
|
|
|
public double HorizontalBorderThickness
|
|
{
|
|
get => GetValue(HorizontalBorderThicknessProperty);
|
|
set => SetValue(HorizontalBorderThicknessProperty, value);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private static readonly FuncTemplate<Panel?> DefaultPanel =
|
|
new(() => new StackPanel { Orientation = Orientation.Vertical });
|
|
|
|
static NavMenu()
|
|
{
|
|
ItemsPanelProperty.OverrideDefaultValue(typeof(NavMenu), DefaultPanel);
|
|
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
|
|
typeof(NavMenu),
|
|
KeyboardNavigationMode.Once);
|
|
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<NavMenu>(AccessibilityView.Control);
|
|
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<NavMenu>(AutomationControlType.Menu);
|
|
AutoScrollToSelectedItemProperty.OverrideDefaultValue<NavMenu>(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<ItemsPresenter>(NavMenuThemeConstants.ItemsPresenterPart);
|
|
SetupMenuItemsPresenter();
|
|
HandleModeChanged();
|
|
}
|
|
} |