diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml index 984bea3..e9760a2 100644 --- a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml @@ -12,7 +12,9 @@ x:DataType="vm:NavMenuDemoViewModel" mc:Ignorable="d"> - + + + Children { get; set; } = new ObservableCollection(); } \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml index 6843a3c..918bdc1 100644 --- a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml +++ b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml @@ -1,17 +1,18 @@ - - - - + + + + - + - - + + @@ -20,10 +21,20 @@ - - + + - + diff --git a/src/Ursa/Controls/NavMenu/NavMenu.cs b/src/Ursa/Controls/NavMenu/NavMenu.cs index 7469e42..38baf33 100644 --- a/src/Ursa/Controls/NavMenu/NavMenu.cs +++ b/src/Ursa/Controls/NavMenu/NavMenu.cs @@ -52,6 +52,17 @@ public class NavMenu: ItemsControl set => SetValue(SubMenuBindingProperty, value); } + public static readonly StyledProperty CommandBindingProperty = AvaloniaProperty.Register( + nameof(CommandBinding)); + + [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IBinding? CommandBinding + { + get => GetValue(CommandBindingProperty); + set => SetValue(CommandBindingProperty, value); + } + static NavMenu() { SelectedItemProperty.Changed.AddClassHandler((o, e) => o.OnSelectedItemChange(e)); @@ -89,18 +100,28 @@ public class NavMenu: ItemsControl { navMenuItem[!ItemsSourceProperty] = SubMenuBinding; } + if (CommandBinding is not null) + { + navMenuItem[!NavMenuItem.CommandProperty] = CommandBinding; + } } } - internal void SelectItem(NavMenuItem item) + internal void SelectItem(NavMenuItem item, NavMenuItem parent) { - if (item.IsSelected) return; - var children = this.LogicalChildren.OfType(); - foreach (var child in children) + // if (item.IsSelected) return; + foreach (var child in LogicalChildren) { - if (child != item) + if (child == parent) { - child.IsSelected = false; + continue; + } + else + { + if (child is NavMenuItem navMenuItem) + { + navMenuItem.ClearSelection(); + } } } if (item.DataContext is not null && item.DataContext != this.DataContext) diff --git a/src/Ursa/Controls/NavMenu/NavMenuItem.cs b/src/Ursa/Controls/NavMenu/NavMenuItem.cs index ccd9ffd..6424278 100644 --- a/src/Ursa/Controls/NavMenu/NavMenuItem.cs +++ b/src/Ursa/Controls/NavMenu/NavMenuItem.cs @@ -11,12 +11,17 @@ using Avalonia.VisualTree; namespace Ursa.Controls; -[PseudoClasses(PC_Highlighted)] +[PseudoClasses(PC_Highlighted, PC_Collapsed, PC_Closed, PC_FirstLevel, PC_Selector)] public class NavMenuItem: HeaderedSelectingItemsControl { - public const string PC_Highlighted = "highlighted"; + public const string PC_Highlighted = ":highlighted"; + public const string PC_FirstLevel = ":first-level"; + public const string PC_Collapsed = ":collapsed"; + public const string PC_Closed = ":closed"; + public const string PC_Selector = ":selector"; private NavMenu? _rootMenu; + private Panel? _popupPanel; public static readonly StyledProperty IconProperty = AvaloniaProperty.Register( nameof(Icon)); @@ -36,8 +41,7 @@ public class NavMenuItem: HeaderedSelectingItemsControl set => SetValue(IconTemplateProperty, value); } - public static readonly StyledProperty CommandProperty = AvaloniaProperty.Register( - nameof(Command)); + public static readonly StyledProperty CommandProperty = Button.CommandProperty.AddOwner(); public ICommand? Command { @@ -45,6 +49,15 @@ public class NavMenuItem: HeaderedSelectingItemsControl set => SetValue(CommandProperty, value); } + public static readonly StyledProperty CommandParameterProperty = + Button.CommandParameterProperty.AddOwner(); + + public object? CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } + public new static readonly StyledProperty IsSelectedProperty = SelectingItemsControl.IsSelectedProperty.AddOwner(); @@ -53,15 +66,34 @@ public class NavMenuItem: HeaderedSelectingItemsControl get => GetValue(IsSelectedProperty); set => SetValue(IsSelectedProperty, value); } + + private bool _isHighlighted; + + public static readonly DirectProperty IsHighlightedProperty = AvaloniaProperty.RegisterDirect( + nameof(IsHighlighted), o => o.IsHighlighted, (o, v) => o.IsHighlighted = v); + + public bool IsHighlighted + { + get => _isHighlighted; + private set => SetAndRaise(IsHighlightedProperty, ref _isHighlighted, value); + } + internal int Level { get; set; } + static NavMenuItem() { SelectableMixin.Attach(IsSelectedProperty); PressedMixin.Attach(); + IsHighlightedProperty.Changed.AddClassHandler((o, e) => o.OnIsHighlightedChange(e)); } - + + private void OnIsHighlightedChange(AvaloniaPropertyChangedEventArgs args) + { + PseudoClasses.Set(PC_Highlighted, args.NewValue.Value); + } + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { return NeedsContainer(item, out recycleKey); @@ -81,6 +113,18 @@ public class NavMenuItem: HeaderedSelectingItemsControl { container[!HeaderProperty] = _rootMenu.HeaderBinding; } + if (_rootMenu?.IconBinding is not null) + { + container[!IconProperty] = _rootMenu.IconBinding; + } + if (_rootMenu?.SubMenuBinding is not null) + { + container[!ItemsSourceProperty] = _rootMenu.SubMenuBinding; + } + if (_rootMenu?.CommandBinding is not null) + { + container[!CommandProperty] = _rootMenu.CommandBinding; + } } } @@ -90,16 +134,76 @@ public class NavMenuItem: HeaderedSelectingItemsControl _rootMenu = GetRootMenu(); } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + var children = this.ItemsPanelRoot?.Children.ToList(); + base.OnApplyTemplate(e); + Level = CalculateDistanceFromLogicalParent(this); + PseudoClasses.Set(PC_FirstLevel, Level == 0); + } + protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); - _rootMenu?.SelectItem(this); + SelectItem(this); + Command?.Execute(CommandParameter); e.Handled = true; } + + protected void SelectItem(NavMenuItem item) + { + if (item == this) + { + SetCurrentValue(IsSelectedProperty, true); + SetCurrentValue(IsHighlightedProperty, true); + } + else + { + SetCurrentValue(IsSelectedProperty, false); + SetCurrentValue(IsHighlightedProperty, true); + } + if (this.Parent is NavMenuItem menuItem) + { + menuItem.SelectItem(item); + } + else if (this.Parent is NavMenu menu) + { + menu.SelectItem(item, this); + } + } + + internal void ClearSelection() + { + SetCurrentValue(IsHighlightedProperty, false); + SetCurrentValue(IsSelectedProperty, false); + foreach (var child in LogicalChildren) + { + if (child is NavMenuItem item) + { + item.ClearSelection(); + } + } + } private NavMenu? GetRootMenu() { var root = this.FindAncestorOfType() ?? this.FindLogicalAncestorOfType(); return root; } + + private static int CalculateDistanceFromLogicalParent(ILogical? logical, int @default = -1) where T : class + { + var result = 0; + + while (logical != null && !(logical is T)) + { + if (logical is NavMenuItem) + { + result++; + } + logical = logical.LogicalParent; + } + + return logical != null ? result : @default; + } } \ No newline at end of file