From bacf1a6330c702c1576ce9f7a295ef684372a41c Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 22 Jun 2023 22:00:28 +0800 Subject: [PATCH] feat: wow, first prototype of navigation menu. --- demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml | 47 +++++++++ .../Pages/NavigationMenuDemo.axaml.cs | 15 +++ .../ViewModels/NavigationMenuDemoViewModel.cs | 41 ++++++++ demo/Ursa.Demo/Views/MainWindow.axaml | 3 + .../Controls/Navigation.axaml | 42 ++++++++ src/Ursa.Themes.Semi/Controls/_index.axaml | 1 + .../Controls/Navigation/NavigationMenu.cs | 68 +++++++++++++ .../Controls/Navigation/NavigationMenuItem.cs | 99 +++++++++++++++++++ .../Navigation/NavigationMenuSeparator.cs | 6 ++ 9 files changed, 322 insertions(+) create mode 100644 demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml.cs create mode 100644 demo/Ursa.Demo/ViewModels/NavigationMenuDemoViewModel.cs create mode 100644 src/Ursa.Themes.Semi/Controls/Navigation.axaml create mode 100644 src/Ursa/Controls/Navigation/NavigationMenu.cs create mode 100644 src/Ursa/Controls/Navigation/NavigationMenuItem.cs create mode 100644 src/Ursa/Controls/Navigation/NavigationMenuSeparator.cs diff --git a/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml new file mode 100644 index 0000000..62112d1 --- /dev/null +++ b/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml.cs b/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml.cs new file mode 100644 index 0000000..0f369fc --- /dev/null +++ b/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml.cs @@ -0,0 +1,15 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Ursa.Demo.ViewModels; + +namespace Ursa.Demo.Pages; + +public partial class NavigationMenuDemo : UserControl +{ + public NavigationMenuDemo() + { + InitializeComponent(); + this.DataContext = new NavigationMenuDemoViewModel(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/NavigationMenuDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/NavigationMenuDemoViewModel.cs new file mode 100644 index 0000000..f9c9bd6 --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/NavigationMenuDemoViewModel.cs @@ -0,0 +1,41 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public class NavigationMenuDemoViewModel: ObservableObject +{ + public ObservableCollection MenuItems { get; set; } = new() + { + new NavigationMenuItemViewModel() + { + MenuHeader = "1", + Children = new ObservableCollection() + { + new NavigationMenuItemViewModel(){ + MenuHeader = "11" , + Children = new ObservableCollection() + { + new NavigationMenuItemViewModel(){MenuHeader = "111"}, + new NavigationMenuItemViewModel(){MenuHeader = "112"} + }}, + new NavigationMenuItemViewModel(){MenuHeader = "12"} + } + }, + new NavigationMenuItemViewModel() + { + MenuHeader = "2", + Children = new ObservableCollection() + { + new NavigationMenuItemViewModel(){MenuHeader = "21"}, + new NavigationMenuItemViewModel(){MenuHeader = "22"} + } + } + }; +} + +public class NavigationMenuItemViewModel: ObservableObject +{ + public string MenuHeader { get; set; } + public ObservableCollection Children { get; set; } = new(); +} \ No newline at end of file diff --git a/demo/Ursa.Demo/Views/MainWindow.axaml b/demo/Ursa.Demo/Views/MainWindow.axaml index 76a2f17..230667b 100644 --- a/demo/Ursa.Demo/Views/MainWindow.axaml +++ b/demo/Ursa.Demo/Views/MainWindow.axaml @@ -35,6 +35,9 @@ + + + diff --git a/src/Ursa.Themes.Semi/Controls/Navigation.axaml b/src/Ursa.Themes.Semi/Controls/Navigation.axaml new file mode 100644 index 0000000..086729e --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/Navigation.axaml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index 48da0db..8cf36e5 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -5,6 +5,7 @@ + diff --git a/src/Ursa/Controls/Navigation/NavigationMenu.cs b/src/Ursa/Controls/Navigation/NavigationMenu.cs new file mode 100644 index 0000000..deadac4 --- /dev/null +++ b/src/Ursa/Controls/Navigation/NavigationMenu.cs @@ -0,0 +1,68 @@ +using System.Collections; +using System.Collections.Specialized; +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Metadata; + +namespace Ursa.Controls; + +public class NavigationMenu: HeaderedSelectingItemsControl +{ + public static readonly StyledProperty FooterProperty = AvaloniaProperty.Register( + nameof(Footer)); + + public object? Footer + { + get => GetValue(FooterProperty); + set => SetValue(FooterProperty, value); + } + + public static readonly StyledProperty FooterTemplateProperty = AvaloniaProperty.Register( + nameof(FooterTemplate)); + + public IDataTemplate FooterTemplate + { + get => GetValue(FooterTemplateProperty); + set => SetValue(FooterTemplateProperty, value); + } + + public static readonly StyledProperty SelectedMenuItemProperty = AvaloniaProperty.Register( + nameof(SelectedMenuItem)); + + public object? SelectedMenuItem + { + get => GetValue(SelectedMenuItemProperty); + set => SetValue(SelectedMenuItemProperty, value); + } + + internal void UpdateSelection(NavigationMenuItem source) + { + var children = this.ItemsPanelRoot?.Children; + if (children is not null) + { + foreach (var child in children) + { + NavigationMenuItem? item = null; + if (child is NavigationMenuItem i) + { + item = i; + } + else if (child is ContentPresenter { Child: NavigationMenuItem i2 }) + { + item = i2; + } + if (item != null) + { + if(Equals(item, source)) continue; + item.SetSelection(null, false, false); + } + } + } + } +} diff --git a/src/Ursa/Controls/Navigation/NavigationMenuItem.cs b/src/Ursa/Controls/Navigation/NavigationMenuItem.cs new file mode 100644 index 0000000..97f1f3e --- /dev/null +++ b/src/Ursa/Controls/Navigation/NavigationMenuItem.cs @@ -0,0 +1,99 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Reactive; +using Avalonia.VisualTree; + +namespace Ursa.Controls; + +[PseudoClasses(PC_Closed, PC_Selected, PC_Empty)] +public class NavigationMenuItem: HeaderedSelectingItemsControl +{ + public const string PC_Closed = ":closed"; + public const string PC_Selected = ":selected"; + public const string PC_Empty = ":empty"; + + private NavigationMenu? _rootMenu; + private IDisposable? _ownerSubscription; + private IDisposable? _itemsBinding; + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _rootMenu = this.FindAncestorOfType(); + if (ItemTemplate == null && _rootMenu?.ItemTemplate != null) + { + SetCurrentValue(ItemTemplateProperty, _rootMenu.ItemTemplate); + } + if (ItemContainerTheme == null && _rootMenu?.ItemContainerTheme != null) + { + SetCurrentValue(ItemContainerThemeProperty, _rootMenu.ItemContainerTheme); + } + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + // Leaf menu node, can be selected. + if (this.ItemCount == 0) + { + var parents = this.GetSelfAndLogicalAncestors(); + if (_rootMenu is not null && parents.Contains(_rootMenu)) + { + object? o = this.DataContext ?? this; + _rootMenu.SelectedMenuItem = o; + } + } + + e.Handled = true; + SetSelection(this, true, true); + } + + internal void SetSelection(NavigationMenuItem? source, bool selected, bool propagateToParent = false) + { + this.PseudoClasses.Set(PC_Selected, selected); + var children = this.ItemsPanelRoot?.Children; + if (children is not null) + { + foreach (var child in children) + { + NavigationMenuItem? item = null; + if (child is NavigationMenuItem i) + { + item = i; + } + else if (child is ContentPresenter { Child: NavigationMenuItem i2 }) + { + item = i2; + } + if (item != null) + { + if(Equals(item, source)) continue; + item.SetSelection(this, false, false); + } + } + } + + if (propagateToParent) + { + var parent = this.FindAncestorOfType(); + if (parent != null) + { + parent.SetSelection(this, selected, true); + } + else + { + if (selected) + { + _rootMenu?.UpdateSelection(this); + } + } + } + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Navigation/NavigationMenuSeparator.cs b/src/Ursa/Controls/Navigation/NavigationMenuSeparator.cs new file mode 100644 index 0000000..ba84405 --- /dev/null +++ b/src/Ursa/Controls/Navigation/NavigationMenuSeparator.cs @@ -0,0 +1,6 @@ +namespace Ursa.Controls; + +public class NavigationMenuSeparator: NavigationMenuItem +{ + +} \ No newline at end of file