using System.Windows.Input; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.VisualTree; using Irihi.Avalonia.Shared.Common; using Irihi.Avalonia.Shared.Helpers; namespace Ursa.Controls; /// /// Navigation Menu Item /// [PseudoClasses(PC_Highlighted, PC_HorizontalCollapsed, PC_VerticalCollapsed, PC_FirstLevel, PC_Selector)] public class NavMenuItem: HeaderedItemsControl { public const string PC_Highlighted = ":highlighted"; public const string PC_FirstLevel = ":first-level"; public const string PC_HorizontalCollapsed = ":horizontal-collapsed"; public const string PC_VerticalCollapsed = ":vertical-collapsed"; public const string PC_Selector = ":selector"; private NavMenu? _rootMenu; private Panel? _popupPanel; private Popup? _popup; private Panel? _overflowPanel; private static readonly Point s_invalidPoint = new (double.NaN, double.NaN); private Point _pointerDownPoint = s_invalidPoint; public static readonly StyledProperty IconProperty = AvaloniaProperty.Register( nameof(Icon)); public object? Icon { get => GetValue(IconProperty); set => SetValue(IconProperty, value); } public static readonly StyledProperty IconTemplateProperty = AvaloniaProperty.Register( nameof(IconTemplate)); public IDataTemplate? IconTemplate { get => GetValue(IconTemplateProperty); set => SetValue(IconTemplateProperty, value); } public static readonly StyledProperty CommandProperty = Button.CommandProperty.AddOwner(); public ICommand? Command { get => GetValue(CommandProperty); set => SetValue(CommandProperty, value); } public static readonly StyledProperty CommandParameterProperty = Button.CommandParameterProperty.AddOwner(); public object? CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); } public static readonly StyledProperty IsSelectedProperty = SelectingItemsControl.IsSelectedProperty.AddOwner(); public bool IsSelected { get => GetValue(IsSelectedProperty); set => SetValue(IsSelectedProperty, value); } public static readonly RoutedEvent IsSelectedChangedEvent = RoutedEvent.Register("IsSelectedChanged", RoutingStrategies.Bubble); private bool _isHighlighted; public static readonly DirectProperty IsHighlightedProperty = AvaloniaProperty.RegisterDirect( nameof(IsHighlighted), o => o.IsHighlighted, (o, v) => o.IsHighlighted = v, defaultBindingMode: BindingMode.TwoWay); public bool IsHighlighted { get => _isHighlighted; private set => SetAndRaise(IsHighlightedProperty, ref _isHighlighted, value); } public static readonly StyledProperty IsHorizontalCollapsedProperty = NavMenu.IsHorizontalCollapsedProperty.AddOwner(); public bool IsHorizontalCollapsed { get => GetValue(IsHorizontalCollapsedProperty); set => SetValue(IsHorizontalCollapsedProperty, value); } public static readonly StyledProperty IsVerticalCollapsedProperty = AvaloniaProperty.Register( nameof(IsVerticalCollapsed)); public bool IsVerticalCollapsed { get => GetValue(IsVerticalCollapsedProperty); set => SetValue(IsVerticalCollapsedProperty, value); } public static readonly StyledProperty SubMenuIndentProperty = NavMenu.SubMenuIndentProperty.AddOwner(); public double SubMenuIndent { get => GetValue(SubMenuIndentProperty); set => SetValue(SubMenuIndentProperty, value); } internal static readonly DirectProperty LevelProperty = AvaloniaProperty.RegisterDirect( nameof(Level), o => o.Level, (o, v) => o.Level = v); private int _level; public int Level { get => _level; set => SetAndRaise(LevelProperty, ref _level, value); } public static readonly StyledProperty IsSeparatorProperty = AvaloniaProperty.Register( nameof(IsSeparator)); public bool IsSeparator { get => GetValue(IsSeparatorProperty); set => SetValue(IsSeparatorProperty, value); } static NavMenuItem() { // SelectableMixin.Attach(IsSelectedProperty); PressedMixin.Attach(); LevelProperty.Changed.AddClassHandler((item, args) => item.OnLevelChange(args)); IsHighlightedProperty.AffectsPseudoClass(PC_Highlighted); IsHorizontalCollapsedProperty.AffectsPseudoClass(PC_HorizontalCollapsed); IsVerticalCollapsedProperty.AffectsPseudoClass(PC_VerticalCollapsed); IsSelectedProperty.AffectsPseudoClass(PseudoClassName.PC_Selected, IsSelectedChangedEvent); IsHorizontalCollapsedProperty.Changed.AddClassHandler((item, args) => item.OnIsHorizontalCollapsedChanged(args)); } private void OnIsHorizontalCollapsedChanged(AvaloniaPropertyChangedEventArgs args) { if (args.NewValue.Value) { if (this.ItemsPanelRoot is OverflowStackPanel s) { s.MoveChildrenToOverflowPanel(); } } else { if (this.ItemsPanelRoot is OverflowStackPanel s) { s.MoveChildrenToMainPanel(); } } } private void OnLevelChange(AvaloniaPropertyChangedEventArgs args) { PseudoClasses.Set(PC_FirstLevel, args.NewValue.Value == 1); } protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { return NeedsContainer(item, out recycleKey); } protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { return new NavMenuItem(); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); _rootMenu = GetRootMenu(); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); SetCurrentValue(LevelProperty,CalculateDistanceFromLogicalParent(this)); _popup = e.NameScope.Find("PART_Popup"); _overflowPanel = e.NameScope.Find("PART_OverflowPanel"); if (_rootMenu is not null) { this.TryBind(IconProperty, _rootMenu.IconBinding); this.TryBind(HeaderProperty, _rootMenu.HeaderBinding); this.TryBind(ItemsSourceProperty, _rootMenu.SubMenuBinding); this.TryBind(CommandProperty, _rootMenu.CommandBinding); this[!IconTemplateProperty] = _rootMenu[!NavMenu.IconTemplateProperty]; this[!HeaderTemplateProperty] = _rootMenu[!NavMenu.HeaderTemplateProperty]; this[!SubMenuIndentProperty] = _rootMenu[!NavMenu.SubMenuIndentProperty]; this[!IsHorizontalCollapsedProperty] = _rootMenu[!NavMenu.IsHorizontalCollapsedProperty]; } } protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); var root = this.ItemsPanelRoot; if (root is OverflowStackPanel stack) { stack.OverflowPanel = _overflowPanel; } } protected override void OnPointerPressed(PointerPressedEventArgs e) { if (IsSeparator) { e.Handled = true; return; } base.OnPointerPressed(e); if (e.Handled) return; var p = e.GetCurrentPoint(this); if (p.Properties.PointerUpdateKind is not (PointerUpdateKind.LeftButtonPressed or PointerUpdateKind.RightButtonPressed)) return; if (p.Pointer.Type == PointerType.Mouse) { if (this.ItemCount == 0) { SelectItem(this); Command?.Execute(CommandParameter); e.Handled = true; } else { if (!IsHorizontalCollapsed) { SetCurrentValue(IsVerticalCollapsedProperty, !IsVerticalCollapsed); e.Handled = true; } else { if (_popup is null || e.Source is not Visual v || _popup.IsInsidePopup(v)) return; if (_popup.IsOpen) { _popup.Close(); } else { _popup.Open(); } } } } else { _pointerDownPoint = p.Position; } } protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); if (!e.Handled && !double.IsNaN(_pointerDownPoint.X) && e.InitialPressMouseButton is MouseButton.Left or MouseButton.Right) { var point = e.GetCurrentPoint(this); if (!new Rect(Bounds.Size).ContainsExclusive(point.Position) || e.Pointer.Type != PointerType.Touch) return; if (this.ItemCount == 0) { SelectItem(this); Command?.Execute(CommandParameter); e.Handled = true; } else { if (!IsHorizontalCollapsed) { SetCurrentValue(IsVerticalCollapsedProperty, !IsVerticalCollapsed); e.Handled = true; } else { if (_popup is null || e.Source is not Visual v || _popup.IsInsidePopup(v)) return; if (_popup.IsOpen) { _popup.Close(); } else { _popup.Open(); } } } } } internal 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); var items = menuItem.LogicalChildren.OfType(); foreach (var child in items) { if (child != this) { child.ClearSelection(); } } } else if (this.Parent is NavMenu menu) { menu.SelectItem(item, this); } if(_popup is not null) { _popup.Close(); } } 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; } internal IEnumerable GetLeafMenus() { if (this.ItemCount == 0) { yield return this; yield break; } foreach (var child in LogicalChildren) { if (child is NavMenuItem item) { var items = item.GetLeafMenus(); foreach (var i in items) { yield return i; } } } } }