Files
AtomUI/src/AtomUI.Controls/NavMenu/NavMenu.cs
T

581 lines
17 KiB
C#

using System.Collections.Specialized;
using System.Reactive.Disposables;
using AtomUI.Controls.Primitives;
using AtomUI.Controls.Themes;
using AtomUI.Data;
using AtomUI.Theme.Data;
using AtomUI.Theme.Styling;
using AtomUI.Utils;
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.Threading;
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
{
#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 NavMenuMode Mode
{
get => GetValue(ModeProperty);
set => SetValue(ModeProperty, value);
}
public bool IsDarkStyle
{
get => GetValue(IsDarkStyleProperty);
set => SetValue(IsDarkStyleProperty, 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));
internal double HorizontalBorderThickness
{
get => GetValue(HorizontalBorderThicknessProperty);
set => SetValue(HorizontalBorderThicknessProperty, value);
}
#endregion
private static readonly FuncTemplate<Panel?> DefaultPanel =
new(() => new StackPanel { Orientation = Orientation.Vertical });
private ItemsPresenter? _menuItemsPresenter;
private readonly Dictionary<NavMenuItem, CompositeDisposable> _itemsBindingDisposables = new();
private IDisposable? _borderThicknessDisposable;
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);
}
public NavMenu()
{
UpdatePseudoClasses();
Items.CollectionChanged += HandleItemsCollectionChanged;
}
private void HandleItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove)
{
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
if (item is NavMenuItem menuItem)
{
if (_itemsBindingDisposables.TryGetValue(menuItem, out var disposable))
{
disposable.Dispose();
}
_itemsBindingDisposables.Remove(menuItem);
}
}
}
}
}
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)
{
var disposables = new CompositeDisposable(6);
disposables.Add(BindUtils.RelayBind(this, ItemContainerThemeProperty, navMenuItem, ItemContainerThemeProperty));
disposables.Add(BindUtils.RelayBind(this, ModeProperty, navMenuItem, NavMenuItem.ModeProperty));
disposables.Add(BindUtils.RelayBind(this, IsDarkStyleProperty, navMenuItem, NavMenuItem.IsDarkStyleProperty));
disposables.Add(BindUtils.RelayBind(this, IsMotionEnabledProperty, navMenuItem, NavMenuItem.IsMotionEnabledProperty));
if (_itemsBindingDisposables.TryGetValue(navMenuItem, out var oldDisposables))
{
oldDisposables.Dispose();
_itemsBindingDisposables.Remove(navMenuItem);
}
_itemsBindingDisposables.Add(navMenuItem, disposables);
}
}
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);
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
ConfigureControlTheme(false);
ConfigureItemContainerTheme(false);
SetupInteractionHandler();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_borderThicknessDisposable = TokenResourceBinder.CreateTokenBinding(this, HorizontalBorderThicknessProperty,
SharedTokenKey.LineWidth,
BindingPriority.Template,
new RenderScaleAwareDoubleConfigure(this));
InteractionHandler?.Attach(this);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_borderThicknessDisposable?.Dispose();
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.HorizontalNavMenuItemThemeId;
}
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();
}
private void ConfigureDefaultOpenedPaths()
{
if (DefaultOpenPaths != null)
{
foreach (var defaultOpenPath in DefaultOpenPaths)
{
var pathNodes = FindMenuItemByPath(defaultOpenPath);
OpenMenuItemPaths(pathNodes);
}
}
}
private void ConfigureDefaultSelectedPath()
{
if (DefaultSelectedPath != null)
{
var pathNodes = FindMenuItemByPath(DefaultSelectedPath);
Dispatcher.UIThread.InvokeAsync(async () =>
{
var navMenuItems = await OpenMenuItemPathAsync(pathNodes);
foreach (var navMenuItem in navMenuItems)
{
navMenuItem.IsSelected = true;
}
});
}
}
private IList<ITreeNode> FindMenuItemByPath(TreeNodePath treeNodePath)
{
if (treeNodePath.Length == 0)
{
return [];
}
var segments = treeNodePath.Segments;
IList<ITreeNode> items = Items.OfType<ITreeNode>().ToList();
IList<ITreeNode> pathNodes = new List<ITreeNode>();
foreach (var segment in segments)
{
bool childFound = false;
for (var i = 0; i < items.Count; i++)
{
var item = items[i];
if (item.ItemKey == segment)
{
items = item.Children;
childFound = true;
pathNodes.Add(item);
break;
}
}
if (!childFound)
{
return [];
}
}
return pathNodes;
}
private void OpenMenuItemPaths(IList<ITreeNode> pathNodes)
{
if (pathNodes.Count == 0)
{
return;
}
Dispatcher.UIThread.InvokeAsync(async () =>
{
await OpenMenuItemPathAsync(pathNodes);
});
}
private async Task<List<NavMenuItem>> OpenMenuItemPathAsync(IList<ITreeNode> pathNodes)
{
List<NavMenuItem> items = new List<NavMenuItem>();
try
{
ItemsControl current = this;
foreach (var pathNode in pathNodes)
{
var child = await GetNavMenuItemContainerAsync(pathNode, current);
if (child != null)
{
items.Add(child);
current = child;
child.IsMotionEnabled = false;
child.IsSubMenuOpen = true;
}
}
}
finally
{
foreach (var item in items)
{
item.IsMotionEnabled = true;
}
}
return items;
}
private async Task<NavMenuItem?> GetNavMenuItemContainerAsync(ITreeNode childNode, ItemsControl current)
{
var cycleCount = 10;
NavMenuItem? target = null;
while (cycleCount > 0)
{
target = current.ContainerFromItem(childNode) as NavMenuItem;
if (target == null)
{
await Task.Delay(TimeSpan.FromMilliseconds(50));
}
else
{
break;
}
--cycleCount;
}
return target;
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
ConfigureDefaultOpenedPaths();
ConfigureDefaultSelectedPath();
}
}