Optimize NavMenu and add NavMenuItemSelectedEvent

This commit is contained in:
chinboy
2025-10-18 19:29:01 +08:00
parent 492db8a6c5
commit 007b2faa71
9 changed files with 131 additions and 129 deletions
@@ -1,22 +0,0 @@
using System.Globalization;
using Avalonia;
using Avalonia.Data.Converters;
namespace AtomUI.Controls.Converters;
internal class DataGridHeaderIndicatorLayoutMarginsConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is Thickness paddings)
{
return new Thickness(0, 0, paddings.Right, 0);
}
return value;
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
@@ -140,15 +140,13 @@ internal class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler
// 判断当前选中的是不是自己
if (!ReferenceEquals(LatestSelectedItem, navMenuItem))
{
var navMenu = Menu as NavMenu;
if (LatestSelectedItem != null)
{
var ancestorInfo = HasCommonAncestor(LatestSelectedItem, navMenuItem);
if (!ancestorInfo.Item1)
{
if (Menu is NavMenu navMenu)
{
navMenu.ClearSelection();
}
navMenu?.ClearSelection();
}
else
{
@@ -160,6 +158,7 @@ internal class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler
}
navMenuItem.SelectItemRecursively();
LatestSelectedItem = navMenuItem;
navMenu?.RaiseNavMenuItemSelected(navMenuItem);
}
}
}
@@ -219,7 +218,7 @@ internal class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler
}
_currentPressedIsValid = false;
if (e.InitialPressMouseButton == MouseButton.Left && item.HasSubMenu == false)
if (e.InitialPressMouseButton == MouseButton.Left)
{
Click(item);
e.Handled = true;
@@ -264,14 +263,25 @@ internal class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler
{
if (e.Source is ILogical control && !Menu.IsLogicalAncestorOf(control))
{
Menu?.Close();
CloseAllTopLevelMenuItems();
}
}
}
protected virtual void WindowDeactivated(object? sender, EventArgs e)
{
Menu?.Close();
CloseAllTopLevelMenuItems();
}
private void CloseAllTopLevelMenuItems()
{
if (Menu != null)
{
foreach (var i in Menu.SubItems)
{
i.Close();
}
}
}
internal static NavMenuItem? GetMenuItem(StyledElement? item) => (NavMenuItem?)GetMenuItemCore(item);
@@ -352,12 +362,14 @@ internal class DefaultNavMenuInteractionHandler : INavMenuInteractionHandler
internal void Click(INavMenuItem item)
{
item.RaiseClick();
var navMenu = FindNavMenu(item);
navMenu?.RaiseNavMenuItemClick(item);
if (!item.StaysOpenOnClick)
if (Menu is NavMenu navMenu)
{
var topLevelItem = FindTopLevelMenuItem(item);
topLevelItem?.Close();
navMenu.RaiseNavMenuItemClick(item);
if (!item.HasSubMenu && !item.StaysOpenOnClick)
{
var topLevelItem = FindTopLevelMenuItem(item);
topLevelItem?.Close();
}
}
}
+17 -2
View File
@@ -1,10 +1,15 @@
using Avalonia.Input;
using AtomUI.Controls.Primitives;
using Avalonia.Rendering;
namespace AtomUI.Controls;
internal interface INavMenu : INavMenuElement, IInputElement
internal interface INavMenu : INavMenuElement
{
/// <summary>
/// Gets or sets the currently selected submenu item.
/// </summary>
INavMenuItem? SelectedItem { get; }
/// <summary>
/// Gets the menu interaction handler.
/// </summary>
@@ -19,4 +24,14 @@ internal interface INavMenu : INavMenuElement, IInputElement
/// Gets the root of the visual tree, if the control is attached to a visual tree.
/// </summary>
IRenderRoot? VisualRoot { get; }
/// <summary>
/// List of paths opened by default
/// </summary>
IList<TreeNodePath>? DefaultOpenPaths { get; set; }
/// <summary>
/// Default selected path
/// </summary>
TreeNodePath? DefaultSelectedPath { get; set; }
}
@@ -5,11 +5,6 @@ namespace AtomUI.Controls;
public interface INavMenuElement : IInputElement, ILogical
{
/// <summary>
/// Gets or sets the currently selected submenu item.
/// </summary>
INavMenuItem? SelectedItem { get; set; }
/// <summary>
/// Gets the submenu items.
/// </summary>
@@ -2,6 +2,11 @@
public interface INavMenuItem : INavMenuElement
{
/// <summary>
/// Gets or sets the currently selected submenu item.
/// </summary>
INavMenuItem? SelectedItem { get; set; }
/// <summary>
/// Gets or sets a value that item key.
/// </summary>
@@ -9,7 +9,7 @@ using NavMenuControl = NavMenu;
internal class InlineNavMenuInteractionHandler : INavMenuInteractionHandler
{
internal INavMenu? NavMenu { get; private set; }
internal INavMenu? Menu { get; private set; }
public void Attach(NavMenu navMenu) => AttachCore(navMenu);
public void Detach(NavMenu navMenu) => DetachCore(navMenu);
@@ -19,24 +19,24 @@ internal class InlineNavMenuInteractionHandler : INavMenuInteractionHandler
internal void AttachCore(INavMenu navMenu)
{
if (NavMenu != null)
if (Menu != null)
{
throw new NotSupportedException("InlineNavMenuInteractionHandler is already attached.");
}
NavMenu = navMenu;
NavMenu.PointerPressed += PointerPressed;
NavMenu.PointerReleased += PointerReleased;
Menu = navMenu;
Menu.PointerPressed += PointerPressed;
Menu.PointerReleased += PointerReleased;
}
internal void DetachCore(INavMenu navMenu)
{
if (NavMenu != navMenu)
if (Menu != navMenu)
{
throw new NotSupportedException("InlineNavMenuInteractionHandler is not attached to the navMenu.");
}
NavMenu.PointerPressed -= PointerPressed;
NavMenu.PointerReleased -= PointerReleased;
NavMenu = null;
Menu.PointerPressed -= PointerPressed;
Menu.PointerReleased -= PointerReleased;
Menu = null;
}
protected virtual void PointerPressed(object? sender, PointerPressedEventArgs e)
@@ -75,15 +75,13 @@ internal class InlineNavMenuInteractionHandler : INavMenuInteractionHandler
// 判断当前选中的是不是自己
if (!ReferenceEquals(LatestSelectedItem, navMenuItem))
{
var navMenu = Menu as NavMenu;
if (LatestSelectedItem != null)
{
var ancestorInfo = HasCommonAncestor(LatestSelectedItem, navMenuItem);
if (!ancestorInfo.Item1)
{
if (NavMenu is NavMenu navMenu)
{
navMenu.ClearSelection();
}
navMenu?.ClearSelection();
}
else
{
@@ -96,6 +94,7 @@ internal class InlineNavMenuInteractionHandler : INavMenuInteractionHandler
navMenuItem.SelectItemRecursively();
LatestSelectedItem = navMenuItem;
navMenu?.RaiseNavMenuItemSelected(navMenuItem);
}
}
}
@@ -156,7 +155,7 @@ internal class InlineNavMenuInteractionHandler : INavMenuInteractionHandler
_currentPressedIsValid = false;
if (e.InitialPressMouseButton == MouseButton.Left && !item.HasSubMenu)
if (e.InitialPressMouseButton == MouseButton.Left)
{
Click(item);
e.Handled = true;
@@ -166,8 +165,10 @@ internal class InlineNavMenuInteractionHandler : INavMenuInteractionHandler
internal void Click(INavMenuItem item)
{
item.RaiseClick();
var navMenu = FindNavMenu(item);
navMenu?.RaiseNavMenuItemClick(item);
if (Menu is NavMenu navMenu)
{
navMenu.RaiseNavMenuItemClick(item);
}
}
internal void Open(INavMenuItem item)
@@ -192,21 +193,4 @@ internal class InlineNavMenuInteractionHandler : INavMenuInteractionHandler
item = item.Parent;
}
}
private static NavMenu? FindNavMenu(INavMenuItem item)
{
var current = (INavMenuElement?)item;
NavMenu? result = null;
while (current != null && !(current is NavMenu))
{
current = (current as INavMenuItem)?.Parent;
}
if (current is NavMenu navMenu)
{
result = navMenu;
}
return result;
}
}
+41 -53
View File
@@ -3,7 +3,6 @@ using System.Reactive.Disposables;
using AtomUI.Controls.Primitives;
using AtomUI.Controls.Themes;
using AtomUI.Data;
using AtomUI.Exceptions;
using AtomUI.Theme;
using AtomUI.Theme.Data;
using AtomUI.Theme.Styling;
@@ -28,60 +27,62 @@ 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 : SelectingItemsControl,
IFocusScope,
public class NavMenu : ItemsControl,
IFocusScope,
INavMenu,
IMotionAwareControl,
IControlSharedTokenResourcesHost
{
#region 公共属性定义
public static readonly DirectProperty<NavMenu, IList<TreeNodePath>?> DefaultOpenPathsProperty =
AvaloniaProperty.RegisterDirect<NavMenu, IList<TreeNodePath>?>(
nameof(DefaultOpenPaths),
o => o.DefaultOpenPaths,
(o, v) => o.DefaultOpenPaths = v);
public static readonly DirectProperty<NavMenu, INavMenuItem?> SelectedItemProperty =
AvaloniaProperty.RegisterDirect<NavMenu, INavMenuItem?>(
nameof(SelectedItem),
o => o.SelectedItem);
public static readonly DirectProperty<NavMenu, TreeNodePath?> DefaultSelectedPathProperty =
AvaloniaProperty.RegisterDirect<NavMenu, TreeNodePath?>(
nameof(DefaultSelectedPath),
o => o.DefaultSelectedPath,
(o, v) => o.DefaultSelectedPath = v);
public static readonly DirectProperty<NavMenu, bool> IsOpenProperty =
AvaloniaProperty.RegisterDirect<NavMenu, bool>(
nameof(IsOpen),
o => o.IsOpen);
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<NavMenu>();
public static readonly StyledProperty<bool> IsAccordionModeProperty =
AvaloniaProperty.Register<NavMenu, bool>(nameof(IsAccordionMode), false);
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 DirectProperty<NavMenu, IList<TreeNodePath>?> DefaultOpenPathsProperty =
AvaloniaProperty.RegisterDirect<NavMenu, IList<TreeNodePath>?>(
nameof(DefaultOpenPaths),
o => o.DefaultOpenPaths,
(o, v) => o.DefaultOpenPaths = v);
public INavMenuItem? _selectedItem;
public INavMenuItem? SelectedItem
{
get => _selectedItem;
private set => SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
}
private IList<TreeNodePath>? _defaultOpenPaths;
public IList<TreeNodePath>? DefaultOpenPaths
@@ -135,9 +136,10 @@ public class NavMenu : SelectingItemsControl,
#region 公共事件定义
public static readonly RoutedEvent<NavMenuItemClickEventArgs> NavMenuItemClickEvent =
RoutedEvent.Register<NavMenu, NavMenuItemClickEventArgs>(
nameof(NavMenuItemClick),
RoutingStrategies.Bubble);
RoutedEvent.Register<NavMenu, NavMenuItemClickEventArgs>(nameof(NavMenuItemClick), RoutingStrategies.Bubble);
public static readonly RoutedEvent<NavMenuItemSelectedEventArgs> NavMenuItemSelectedEvent =
RoutedEvent.Register<NavMenu, NavMenuItemSelectedEventArgs>(nameof(NavMenuItemSelected), RoutingStrategies.Bubble);
public static readonly RoutedEvent<RoutedEventArgs> OpenedEvent =
RoutedEvent.Register<NavMenu, RoutedEventArgs>(nameof(Opened), RoutingStrategies.Bubble);
@@ -151,6 +153,12 @@ public class NavMenu : SelectingItemsControl,
remove => RemoveHandler(NavMenuItemClickEvent, value);
}
public event EventHandler<NavMenuItemSelectedEventArgs>? NavMenuItemSelected
{
add => AddHandler(NavMenuItemSelectedEvent, value);
remove => RemoveHandler(NavMenuItemSelectedEvent, value);
}
public event EventHandler<RoutedEventArgs>? Opened
{
add => AddHandler(OpenedEvent, value);
@@ -185,17 +193,6 @@ public class NavMenu : SelectingItemsControl,
IRenderRoot? INavMenu.VisualRoot => VisualRoot;
INavMenuItem? INavMenuElement.SelectedItem
{
get
{
var index = SelectedIndex;
return (index != -1) ? (INavMenuItem?)ContainerFromIndex(index) : null;
}
set => SelectedIndex = value is Control c ? IndexFromContainer(c) : -1;
}
IEnumerable<INavMenuItem> INavMenuElement.SubItems => LogicalChildren.OfType<INavMenuItem>();
/// <summary>
@@ -217,7 +214,6 @@ public class NavMenu : SelectingItemsControl,
KeyboardNavigationMode.Once);
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<NavMenu>(AccessibilityView.Control);
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<NavMenu>(AutomationControlType.Menu);
AutoScrollToSelectedItemProperty.OverrideDefaultValue<NavMenu>(false);
NavMenuItem.SubmenuOpenedEvent.AddClassHandler<NavMenu>((x, e) => x.OnSubmenuOpened(e));
}
@@ -240,7 +236,7 @@ public class NavMenu : SelectingItemsControl,
}
IsOpen = false;
SelectedIndex = -1;
SelectedItem = null;
RaiseEvent(new RoutedEventArgs
{
@@ -286,15 +282,6 @@ public class NavMenu : SelectingItemsControl,
}
IsOpen = true;
}
private void ValidateSelectionMode()
{
if (SelectionMode.HasFlag(SelectionMode.Multiple))
{
throw new InvalidPropertyValueException(SelectionModeProperty.Name, SelectionMode.Multiple,
$"The value '{SelectionMode.Multiple}' is invalid for the '{SelectionModeProperty.Name}' property in NavMenuItem.");
}
}
private void HandleItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
@@ -331,10 +318,6 @@ public class NavMenu : SelectingItemsControl,
}
}
}
else if (change.Property == SelectionModeProperty)
{
ValidateSelectionMode();
}
if (change.Property == IsDarkStyleProperty ||
change.Property == ModeProperty)
{
@@ -660,6 +643,11 @@ public class NavMenu : SelectingItemsControl,
RaiseEvent(new NavMenuItemClickEventArgs(NavMenuItemClickEvent, navMenuItem));
}
internal void RaiseNavMenuItemSelected(INavMenuItem navMenuItem)
{
RaiseEvent(new NavMenuItemSelectedEventArgs(NavMenuItemSelectedEvent, navMenuItem));
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
@@ -0,0 +1,25 @@
using Avalonia.Interactivity;
namespace AtomUI.Controls;
public class NavMenuItemClickEventArgs : RoutedEventArgs
{
public NavMenuItemClickEventArgs(RoutedEvent routedEvent, INavMenuItem navMenuItem)
: base(routedEvent)
{
NavMenuItem = navMenuItem;
}
public INavMenuItem NavMenuItem { get; }
}
public class NavMenuItemSelectedEventArgs : RoutedEventArgs
{
public NavMenuItemSelectedEventArgs(RoutedEvent routedEvent, INavMenuItem navMenuItem)
: base(routedEvent)
{
NavMenuItem = navMenuItem;
}
public INavMenuItem NavMenuItem { get; }
}
+2 -2
View File
@@ -191,12 +191,12 @@ public class NavMenuItem : HeaderedSelectingItemsControl,
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
INavMenuItem? INavMenuElement.SelectedItem
INavMenuItem? INavMenuItem.SelectedItem
{
get
{
var index = SelectedIndex;
return (index != -1) ? (INavMenuItem?)ContainerFromIndex(index) : null;
return index != -1 ? (INavMenuItem?)ContainerFromIndex(index) : null;
}
set => SelectedIndex = value is Control c ? IndexFromContainer(c) : -1;