From bacf1a6330c702c1576ce9f7a295ef684372a41c Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 22 Jun 2023 22:00:28 +0800 Subject: [PATCH 01/11] 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 From c21e571b74ebc3ba376fd2b1fa5d4db016011885 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 23 Jun 2023 02:46:37 +0800 Subject: [PATCH 02/11] feat: update to finish functionality, start to build theme. --- .../Ursa.Demo/Converters/IconNameConverter.cs | 32 ++++ demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml | 39 ++++- .../ViewModels/NavigationMenuDemoViewModel.cs | 22 ++- .../Controls/Navigation.axaml | 163 ++++++++++++++++-- ...avigationMenuItemLevelToMarginConverter.cs | 23 +++ .../Controls/Navigation/NavigationMenu.cs | 112 ++++++++++-- .../Controls/Navigation/NavigationMenuItem.cs | 146 ++++++++++++++-- 7 files changed, 475 insertions(+), 62 deletions(-) create mode 100644 demo/Ursa.Demo/Converters/IconNameConverter.cs create mode 100644 src/Ursa.Themes.Semi/Converters/NavigationMenuItemLevelToMarginConverter.cs diff --git a/demo/Ursa.Demo/Converters/IconNameConverter.cs b/demo/Ursa.Demo/Converters/IconNameConverter.cs new file mode 100644 index 0000000..309a0e8 --- /dev/null +++ b/demo/Ursa.Demo/Converters/IconNameConverter.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia; +using Avalonia.Controls.Shapes; +using Avalonia.Data.Converters; +using Avalonia.Media; +using Avalonia.Metadata; + +namespace Ursa.Demo.Converters; + +public class IconNameConverter: IValueConverter +{ + [Content] + public Dictionary Paths { get; set; } = new(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is null) return null; + if (value is string s) + { + return Paths.TryGetValue(s, out var path)? path: AvaloniaProperty.UnsetValue; + } + return AvaloniaProperty.UnsetValue; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml index 62112d1..126b08f 100644 --- a/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml @@ -2,6 +2,7 @@ x:Class="Ursa.Demo.Pages.NavigationMenuDemo" xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:converters="clr-namespace:Ursa.Demo.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:u="https://irihi.tech/ursa" @@ -11,12 +12,38 @@ x:CompileBindings="True" x:DataType="vm:NavigationMenuDemoViewModel" mc:Ignorable="d"> - - + + + M12 16C13.9818 16 15.7453 14.3394 16.7142 11.8589C17.3163 11.6122 17.8892 10.8644 18.1508 9.88823C18.4909 8.61881 18.4234 7.48536 17.4964 7.13266C17.4064 2.7111 15.6617 1 12 1C8.33858 1 6.59387 2.71088 6.50372 7.13179C5.57454 7.48354 5.50668 8.61777 5.84709 9.8882C6.10904 10.8658 6.68318 11.6143 7.28626 11.8599C8.2552 14.3398 10.0186 16 12 16Z M19.6049 22C20.8385 22 21.7171 20.8487 20.867 19.9547C19.1971 18.1985 15.853 17 12 17C8.14699 17 4.80292 18.1985 3.133 19.9547C2.2829 20.8487 3.16148 22 4.39513 22H19.6049Z + M10.7525 1.90411C11.1451 0.698628 12.8549 0.698631 13.2475 1.90411L15.2395 8.01946H21.6858C22.9565 8.01946 23.4848 9.64143 22.4568 10.3865L17.2417 14.1659L19.2337 20.2813C19.6263 21.4868 18.2431 22.4892 17.2151 21.7442L12 17.9647L6.78489 21.7442C5.75687 22.4892 4.37368 21.4868 4.76635 20.2813L6.75834 14.1659L1.54323 10.3865C0.515206 9.64142 1.04354 8.01946 2.31425 8.01946H8.76048L10.7525 1.90411Z + M7.99973 5.07197C7.19713 5.53535 6.20729 5.53113 5.40866 5.06092L5.1637 4.91669C4.55751 4.55978 3.77662 4.65563 3.34264 5.20927C2.69567 6.03462 2.17585 6.94251 1.79166 7.90124C1.53027 8.55354 1.83733 9.27693 2.449 9.62286L2.69407 9.76145C3.50107 10.2178 4.00002 11.0732 4.00002 12.0003C4.00002 12.9271 3.50145 13.7822 2.69492 14.2387L2.44842 14.3783C1.83596 14.725 1.52888 15.4497 1.79213 16.1024C1.98358 16.577 2.21048 17.044 2.47374 17.5C2.73723 17.9564 3.0285 18.3868 3.34416 18.7902C3.77773 19.3443 4.5588 19.4406 5.16498 19.0834L5.40839 18.9399C6.20714 18.4692 7.19739 18.4648 8.0003 18.9284C8.80291 19.3918 9.29417 20.2511 9.28627 21.1778L9.28386 21.4601C9.27787 22.1629 9.75107 22.7906 10.4468 22.8903C11.4692 23.0368 12.5154 23.0404 13.5537 22.8927C14.2499 22.7936 14.7231 22.1653 14.7169 21.462L14.7143 21.1785C14.7061 20.2514 15.1974 19.3916 16.0003 18.928C16.8029 18.4647 17.7927 18.4689 18.5914 18.9391L18.8363 19.0833C19.4425 19.4402 20.2234 19.3444 20.6574 18.7907C21.3044 17.9654 21.8242 17.0575 22.2084 16.0988C22.4698 15.4465 22.1627 14.7231 21.551 14.3772L21.306 14.2386C20.499 13.7822 20 12.9268 20 11.9997C20 11.0729 20.4986 10.2178 21.3051 9.76126L21.5516 9.62174C22.1641 9.27506 22.4712 8.55029 22.2079 7.89761C22.0165 7.42297 21.7896 6.95598 21.5263 6.50001C21.2628 6.04362 20.9715 5.61325 20.6559 5.20982C20.2223 4.65568 19.4412 4.55944 18.8351 4.91665L18.5916 5.06009C17.7929 5.53078 16.8026 5.53519 15.9997 5.07163C15.1971 4.60825 14.7059 3.74891 14.7138 2.82218L14.7162 2.53994C14.7222 1.83708 14.249 1.20945 13.5532 1.10973C12.5308 0.963214 11.4846 0.959581 10.4464 1.10733C9.75011 1.20641 9.27691 1.83473 9.28317 2.53798L9.28569 2.82154C9.29395 3.74862 8.80264 4.60841 7.99973 5.07197ZM14 15.4641C15.9132 14.3595 16.5687 11.9132 15.4641 9.99999C14.3595 8.08682 11.9132 7.43132 10 8.53589C8.08684 9.64046 7.43134 12.0868 8.53591 14C9.64048 15.9132 12.0868 16.5687 14 15.4641Z + + + + + + + + - - - + + + + + + + @@ -28,7 +55,7 @@ - + diff --git a/demo/Ursa.Demo/ViewModels/NavigationMenuDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/NavigationMenuDemoViewModel.cs index f9c9bd6..8a394ba 100644 --- a/demo/Ursa.Demo/ViewModels/NavigationMenuDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/NavigationMenuDemoViewModel.cs @@ -9,26 +9,29 @@ public class NavigationMenuDemoViewModel: ObservableObject { new NavigationMenuItemViewModel() { - MenuHeader = "1", + MenuHeader = "任务管理", + MenuIconName = "User", Children = new ObservableCollection() { - new NavigationMenuItemViewModel(){ - MenuHeader = "11" , + new (){ + MenuHeader = "公告管理" , + MenuIconName = "Star", Children = new ObservableCollection() { - new NavigationMenuItemViewModel(){MenuHeader = "111"}, - new NavigationMenuItemViewModel(){MenuHeader = "112"} + new () {MenuHeader = "公告设置"}, + new () {MenuHeader = "公告处理"} }}, - new NavigationMenuItemViewModel(){MenuHeader = "12"} + new (){MenuHeader = "任务查询"} } }, new NavigationMenuItemViewModel() { - MenuHeader = "2", + MenuHeader = "任务平台", + MenuIconName = "Gear", Children = new ObservableCollection() { - new NavigationMenuItemViewModel(){MenuHeader = "21"}, - new NavigationMenuItemViewModel(){MenuHeader = "22"} + new (){MenuHeader = "任务管理"}, + new (){MenuHeader = "用户任务查询"} } } }; @@ -37,5 +40,6 @@ public class NavigationMenuDemoViewModel: ObservableObject public class NavigationMenuItemViewModel: ObservableObject { public string MenuHeader { get; set; } + public string MenuIconName { get; set; } public ObservableCollection Children { get; set; } = new(); } \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/Navigation.axaml b/src/Ursa.Themes.Semi/Controls/Navigation.axaml index 086729e..329fabb 100644 --- a/src/Ursa.Themes.Semi/Controls/Navigation.axaml +++ b/src/Ursa.Themes.Semi/Controls/Navigation.axaml @@ -1,42 +1,171 @@ + + + + - - - - - + + + + + + + + + + + + + + + + + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Converters/NavigationMenuItemLevelToMarginConverter.cs b/src/Ursa.Themes.Semi/Converters/NavigationMenuItemLevelToMarginConverter.cs new file mode 100644 index 0000000..33d63f4 --- /dev/null +++ b/src/Ursa.Themes.Semi/Converters/NavigationMenuItemLevelToMarginConverter.cs @@ -0,0 +1,23 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace Ursa.Themes.Semi.Converters; + +public class NavigationMenuItemLevelToMarginConverter: IValueConverter +{ + public int Indent { get; set; } + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is int i) + { + return new Thickness((i-1) * Indent, 0, 0, 0); + } + return new Thickness(); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Navigation/NavigationMenu.cs b/src/Ursa/Controls/Navigation/NavigationMenu.cs index deadac4..478c556 100644 --- a/src/Ursa/Controls/Navigation/NavigationMenu.cs +++ b/src/Ursa/Controls/Navigation/NavigationMenu.cs @@ -3,6 +3,7 @@ using System.Collections.Specialized; using Avalonia; using Avalonia.Collections; using Avalonia.Controls; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -12,8 +13,14 @@ using Avalonia.Metadata; namespace Ursa.Controls; -public class NavigationMenu: HeaderedSelectingItemsControl +[PseudoClasses(PC_Closed)] +[TemplatePart(Name = PART_CloseButton, Type = typeof(ToggleButton))] + +public class NavigationMenu: HeaderedItemsControl { + public const string PC_Closed = ":closed"; + public const string PART_CloseButton = "PART_CloseButton"; + public static readonly StyledProperty FooterProperty = AvaloniaProperty.Register( nameof(Footer)); @@ -32,13 +39,82 @@ public class NavigationMenu: HeaderedSelectingItemsControl set => SetValue(FooterTemplateProperty, value); } - public static readonly StyledProperty SelectedMenuItemProperty = AvaloniaProperty.Register( - nameof(SelectedMenuItem)); + public static readonly StyledProperty IconProperty = AvaloniaProperty.Register( + nameof(Icon)); - public object? SelectedMenuItem + public object? Icon { - get => GetValue(SelectedMenuItemProperty); - set => SetValue(SelectedMenuItemProperty, value); + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + + public static readonly StyledProperty SelectedItemProperty = AvaloniaProperty.Register( + nameof(SelectedItem)); + + public object? SelectedItem + { + get => GetValue(SelectedItemProperty); + set => SetValue(SelectedItemProperty, value); + } + + public static readonly StyledProperty ShowCollapseButtonProperty = AvaloniaProperty.Register( + nameof(ShowCollapseButton)); + + public bool ShowCollapseButton + { + get => GetValue(ShowCollapseButtonProperty); + set => SetValue(ShowCollapseButtonProperty, value); + } + + public static readonly StyledProperty IsClosedProperty = AvaloniaProperty.Register( + nameof(IsClosed)); + + public bool IsClosed + { + get => GetValue(IsClosedProperty); + set => SetValue(IsClosedProperty, value); + } + + public static readonly StyledProperty OpenedWidthProperty = AvaloniaProperty.Register( + nameof(OpenedWidth)); + + public double OpenedWidth + { + get => GetValue(OpenedWidthProperty); + set => SetValue(OpenedWidthProperty, value); + } + + public static readonly StyledProperty ClosedWidthProperty = AvaloniaProperty.Register( + nameof(ClosedWidth)); + + public double ClosedWidth + { + get => GetValue(ClosedWidthProperty); + set => SetValue(ClosedWidthProperty, value); + } + + + + static NavigationMenu() + { + SelectedItemProperty.Changed.AddClassHandler((o, e) => o.OnSelectionItemChanged(e)); + IsClosedProperty.Changed.AddClassHandler((o,e)=>o.OnIsClosedChanged(e)); + } + + private void OnSelectionItemChanged(AvaloniaPropertyChangedEventArgs args) + { + var newItem = args.GetNewValue(); + if (newItem is not null) + { + UpdateSelectionFromSelectedItem(newItem); + } + } + + private void OnIsClosedChanged(AvaloniaPropertyChangedEventArgs args) + { + bool newValue = args.GetNewValue(); + PseudoClasses.Set(PC_Closed, newValue); } internal void UpdateSelection(NavigationMenuItem source) @@ -48,15 +124,7 @@ public class NavigationMenu: HeaderedSelectingItemsControl { 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; - } + NavigationMenuItem? item = NavigationMenuItem.GetMenuItemFromControl(child); if (item != null) { if(Equals(item, source)) continue; @@ -65,4 +133,18 @@ public class NavigationMenu: HeaderedSelectingItemsControl } } } + + private void UpdateSelectionFromSelectedItem(object? o) + { + var children = this.ItemsPanelRoot?.Children; + if (children is not null) + { + foreach (var child in children) + { + NavigationMenuItem? item = NavigationMenuItem.GetMenuItemFromControl(child); + if(item is null) continue; + item.UpdateSelectionFromSelectedItem(o); + } + } + } } diff --git a/src/Ursa/Controls/Navigation/NavigationMenuItem.cs b/src/Ursa/Controls/Navigation/NavigationMenuItem.cs index 97f1f3e..84019d1 100644 --- a/src/Ursa/Controls/Navigation/NavigationMenuItem.cs +++ b/src/Ursa/Controls/Navigation/NavigationMenuItem.cs @@ -1,8 +1,10 @@ +using System.Security.Cryptography.X509Certificates; using Avalonia; 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.LogicalTree; @@ -12,16 +14,68 @@ using Avalonia.VisualTree; namespace Ursa.Controls; -[PseudoClasses(PC_Closed, PC_Selected, PC_Empty)] +[PseudoClasses(PC_Closed, PC_Selected, PC_Highlighted, PC_Collapsed)] public class NavigationMenuItem: HeaderedSelectingItemsControl { public const string PC_Closed = ":closed"; public const string PC_Selected = ":selected"; - public const string PC_Empty = ":empty"; + public const string PC_Highlighted= ":highlighted"; + public const string PC_Collapsed = ":collapsed"; private NavigationMenu? _rootMenu; private IDisposable? _ownerSubscription; private IDisposable? _itemsBinding; + private bool _isCollapsed; + + public static readonly StyledProperty IsClosedProperty = AvaloniaProperty.Register( + nameof(IsClosed)); + + public bool IsClosed + { + get => GetValue(IsClosedProperty); + set => SetValue(IsClosedProperty, value); + } + + 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); + } + + private int _level; + + public static readonly DirectProperty LevelProperty = AvaloniaProperty.RegisterDirect( + nameof(Level), o => o.Level); + + public int Level + { + get => _level; + private set => SetAndRaise(LevelProperty, ref _level, value); + } + + static NavigationMenuItem() + { + IsClosedProperty.Changed.AddClassHandler((o, e) => o.OnIsClosedChanged(e)); + } + + private void OnIsClosedChanged(AvaloniaPropertyChangedEventArgs args) + { + bool newValue = args.GetNewValue(); + PseudoClasses.Set(PC_Closed, newValue); + } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -35,6 +89,11 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl { SetCurrentValue(ItemContainerThemeProperty, _rootMenu.ItemContainerTheme); } + + _rootMenu?.GetObservable(NavigationMenu.IsClosedProperty) + .Subscribe(new AnonymousObserver(a => this.IsClosed = a)); + + Level = CalculateDistanceFromLogicalParent(this) - 1; } protected override void OnPointerPressed(PointerPressedEventArgs e) @@ -47,31 +106,38 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl if (_rootMenu is not null && parents.Contains(_rootMenu)) { object? o = this.DataContext ?? this; - _rootMenu.SelectedMenuItem = o; + _rootMenu.SelectedItem = o; } + SetSelection(this, true, true); + } + // Non-leaf node, act as a toggle button. + else + { + _isCollapsed = !_isCollapsed; + this.PseudoClasses.Set(PC_Collapsed, _isCollapsed); } - e.Handled = true; - SetSelection(this, true, true); + } internal void SetSelection(NavigationMenuItem? source, bool selected, bool propagateToParent = false) { - this.PseudoClasses.Set(PC_Selected, selected); + if (Equals(this, source) && this.ItemCount == 0) + { + this.PseudoClasses.Set(PC_Highlighted, selected); + this.PseudoClasses.Set(PC_Selected, selected); + } + else + { + this.PseudoClasses.Set(PC_Selected, false); + this.PseudoClasses.Set(PC_Highlighted, 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; - } + NavigationMenuItem? item = GetMenuItemFromControl(child); if (item != null) { if(Equals(item, source)) continue; @@ -96,4 +162,54 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl } } } + + internal void UpdateSelectionFromSelectedItem(object? o) + { + if (o is null) + { + this.SetSelection(this, false, false); + return; + } + + if (Equals(this, o) || Equals(this.DataContext, o)) + { + this.SetSelection(this, true, true); + } + else + { + var children = this.ItemsPanelRoot?.Children; + if (children is not null) + { + foreach (var child in children) + { + NavigationMenuItem? item = GetMenuItemFromControl(child); + if (item != null) + { + item.UpdateSelectionFromSelectedItem(o); + } + } + } + } + } + + private static int CalculateDistanceFromLogicalParent(ILogical? logical, int @default = -1) where T : class + { + var result = 0; + + while (logical != null && !(logical is T)) + { + ++result; + logical = logical.LogicalParent; + } + + return logical != null ? result : @default; + } + + public static NavigationMenuItem? GetMenuItemFromControl(Control? control) + { + if (control is null) return null; + if (control is NavigationMenuItem item) return item; + if (control is ContentPresenter { Child: NavigationMenuItem item2 }) return item2; + return null; + } } \ No newline at end of file From f84a482394012edab1172be6100e5f9ac9d70262 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 23 Jun 2023 03:32:50 +0800 Subject: [PATCH 03/11] feat: update resources. --- demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml | 31 ++----- .../Controls/Navigation.axaml | 86 ++++++++++++++----- .../Themes/Dark/NavigationMenu.axaml | 9 ++ src/Ursa.Themes.Semi/Themes/Dark/_index.axaml | 1 + .../Themes/Light/NavigationMenu.axaml | 9 ++ .../Themes/Light/_index.axaml | 1 + .../Controls/Navigation/NavigationMenuItem.cs | 2 + 7 files changed, 92 insertions(+), 47 deletions(-) create mode 100644 src/Ursa.Themes.Semi/Themes/Dark/NavigationMenu.axaml create mode 100644 src/Ursa.Themes.Semi/Themes/Light/NavigationMenu.axaml diff --git a/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml index 126b08f..dad4c0b 100644 --- a/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml @@ -24,9 +24,14 @@ + + + - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Ursa.Themes.Semi/Controls/Navigation.axaml b/src/Ursa.Themes.Semi/Controls/Navigation.axaml index 329fabb..b0950d3 100644 --- a/src/Ursa.Themes.Semi/Controls/Navigation.axaml +++ b/src/Ursa.Themes.Semi/Controls/Navigation.axaml @@ -30,20 +30,19 @@ Background="{TemplateBinding Background}" RowDefinitions="Auto, *, Auto, Auto"> + ContentTemplate="{TemplateBinding HeaderTemplate}" /> @@ -52,13 +51,29 @@ Grid.Row="2" Content="{TemplateBinding Footer}" ContentTemplate="{TemplateBinding FooterTemplate}" /> - + IsVisible="{TemplateBinding ShowCollapseButton}" + Theme="{DynamicResource ButtonToggleSwitch}"> + + + + + + + @@ -68,7 +83,7 @@ + + + - - - + + - - - + + + + diff --git a/src/Ursa.Themes.Semi/Converters/NavigationMenuItemLevelToMarginConverter.cs b/src/Ursa.Themes.Semi/Converters/NavigationMenuItemLevelToMarginConverter.cs index 33d63f4..bcacb57 100644 --- a/src/Ursa.Themes.Semi/Converters/NavigationMenuItemLevelToMarginConverter.cs +++ b/src/Ursa.Themes.Semi/Converters/NavigationMenuItemLevelToMarginConverter.cs @@ -4,20 +4,20 @@ using Avalonia.Data.Converters; namespace Ursa.Themes.Semi.Converters; -public class NavigationMenuItemLevelToMarginConverter: IValueConverter +public class NavigationMenuItemLevelToMarginConverter: IMultiValueConverter { public int Indent { get; set; } - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { - if (value is int i) + if (values[0] is int i && values[1] is bool b) { + if (b) + { + return new Thickness(); + } return new Thickness((i-1) * Indent, 0, 0, 0); } return new Thickness(); } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } } \ No newline at end of file diff --git a/src/Ursa/Controls/Navigation/NavigationMenu.cs b/src/Ursa/Controls/Navigation/NavigationMenu.cs index 478c556..52575b6 100644 --- a/src/Ursa/Controls/Navigation/NavigationMenu.cs +++ b/src/Ursa/Controls/Navigation/NavigationMenu.cs @@ -134,7 +134,7 @@ public class NavigationMenu: HeaderedItemsControl } } - private void UpdateSelectionFromSelectedItem(object? o) + internal void UpdateSelectionFromSelectedItem(object? o) { var children = this.ItemsPanelRoot?.Children; if (children is not null) diff --git a/src/Ursa/Controls/Navigation/NavigationMenuItem.cs b/src/Ursa/Controls/Navigation/NavigationMenuItem.cs index dfc2963..946af82 100644 --- a/src/Ursa/Controls/Navigation/NavigationMenuItem.cs +++ b/src/Ursa/Controls/Navigation/NavigationMenuItem.cs @@ -15,18 +15,22 @@ using Avalonia.VisualTree; namespace Ursa.Controls; -[PseudoClasses(PC_Closed, PC_Selected, PC_Highlighted, PC_Collapsed)] +[PseudoClasses(PC_Closed, PC_Selected, PC_Highlighted, PC_Collapsed, PC_TopLevel)] +[TemplatePart(PART_Popup, typeof(Popup))] public class NavigationMenuItem: HeaderedSelectingItemsControl { public const string PC_Closed = ":closed"; public const string PC_Selected = ":selected"; public const string PC_Highlighted= ":highlighted"; public const string PC_Collapsed = ":collapsed"; + public const string PC_TopLevel = ":top-level"; + public const string PART_Popup = "PART_Popup"; private NavigationMenu? _rootMenu; private IDisposable? _ownerSubscription; private IDisposable? _itemsBinding; private bool _isCollapsed; + private Popup? _popup; public static readonly StyledProperty IsClosedProperty = AvaloniaProperty.Register( nameof(IsClosed)); @@ -54,18 +58,16 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl get => GetValue(IconTemplateProperty); set => SetValue(IconTemplateProperty, value); } - - private int _level; - + public static readonly DirectProperty LevelProperty = AvaloniaProperty.RegisterDirect( nameof(Level), o => o.Level); - + private int _level; public int Level { get => _level; private set => SetAndRaise(LevelProperty, ref _level, value); } - + public static readonly StyledProperty CommandProperty = AvaloniaProperty.Register( nameof(Command)); @@ -84,6 +86,24 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl set => SetValue(CommandParameterProperty, value); } + public static readonly DirectProperty IsTopLevelMenuItemProperty = AvaloniaProperty.RegisterDirect( + nameof(IsTopLevelMenuItem), o => o.IsTopLevelMenuItem, (o, v) => o.IsTopLevelMenuItem = v); + private bool _isTopLevelMenuItem; + public bool IsTopLevelMenuItem + { + get => _isTopLevelMenuItem; + set => SetAndRaise(IsTopLevelMenuItemProperty, ref _isTopLevelMenuItem, value); + } + + public static readonly StyledProperty IsPopupOpenProperty = AvaloniaProperty.Register( + nameof(IsPopupOpen)); + + public bool IsPopupOpen + { + get => GetValue(IsPopupOpenProperty); + set => SetValue(IsPopupOpenProperty, value); + } + static NavigationMenuItem() { IsClosedProperty.Changed.AddClassHandler((o, e) => o.OnIsClosedChanged(e)); @@ -96,13 +116,6 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl PseudoClasses.Set(PC_Closed, newValue); } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - - } - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); @@ -116,10 +129,19 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl SetCurrentValue(ItemContainerThemeProperty, _rootMenu.ItemContainerTheme); } + if (_rootMenu is not null) + { + IsClosed = _rootMenu.IsClosed; + } + _rootMenu?.GetObservable(NavigationMenu.IsClosedProperty) .Subscribe(new AnonymousObserver(a => this.IsClosed = a)); - + _rootMenu?.UpdateSelectionFromSelectedItem(_rootMenu.SelectedItem); + _popup = e.NameScope.Find(PART_Popup); Level = CalculateDistanceFromLogicalParent(this) - 1; + bool isTopLevel = Level == 1; + IsTopLevelMenuItem = isTopLevel; + PseudoClasses.Set(PC_TopLevel, isTopLevel); } private void GetRootMenu() @@ -127,11 +149,10 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl _rootMenu = this.FindAncestorOfType(); if (_rootMenu is null) { - var popupRoot = TopLevel.GetTopLevel(this) as PopupRoot; - if (popupRoot?.Parent is Popup popup) + var parents = this.FindLogicalAncestorOfType(); + if (parents is not null) { - Control? c = popup.PlacementTarget; - _rootMenu = c.FindAncestorOfType(); + _rootMenu = parents; } } } @@ -142,8 +163,7 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl // Leaf menu node, can be selected. if (this.ItemCount == 0) { - var parents = this.GetSelfAndLogicalAncestors(); - if (_rootMenu is not null && parents.Contains(_rootMenu)) + if (_rootMenu is not null ) { object? o = this.DataContext ?? this; _rootMenu.SelectedItem = o; @@ -155,6 +175,10 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl { _isCollapsed = !_isCollapsed; this.PseudoClasses.Set(PC_Collapsed, _isCollapsed); + if (_popup is not null) + { + _popup.IsOpen = !_popup.IsOpen; + } } e.Handled = true; Command?.Execute(CommandParameter); @@ -188,7 +212,7 @@ public class NavigationMenuItem: HeaderedSelectingItemsControl if (propagateToParent) { - var parent = this.FindAncestorOfType(); + var parent = this.FindLogicalAncestorOfType(); if (parent != null) { parent.SetSelection(this, selected, true); From 1b88fcff25ab443d5abbd792de5e5db64ffcdcfd Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 23 Jun 2023 13:36:11 +0800 Subject: [PATCH 08/11] feat: update visual --- src/Ursa.Themes.Semi/Controls/Navigation.axaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ursa.Themes.Semi/Controls/Navigation.axaml b/src/Ursa.Themes.Semi/Controls/Navigation.axaml index 421ad30..4d35613 100644 --- a/src/Ursa.Themes.Semi/Controls/Navigation.axaml +++ b/src/Ursa.Themes.Semi/Controls/Navigation.axaml @@ -123,6 +123,7 @@ - + Date: Fri, 23 Jun 2023 14:32:15 +0800 Subject: [PATCH 10/11] feat: implement separator. --- .../Converters/MenuDataTemplateSelector.cs | 27 +++++++ demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml | 73 +++++++++++-------- .../ViewModels/NavigationMenuDemoViewModel.cs | 7 ++ .../Controls/Navigation.axaml | 36 +++++++++ .../Navigation/NavigationMenuSeparator.cs | 7 +- 5 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 demo/Ursa.Demo/Converters/MenuDataTemplateSelector.cs diff --git a/demo/Ursa.Demo/Converters/MenuDataTemplateSelector.cs b/demo/Ursa.Demo/Converters/MenuDataTemplateSelector.cs new file mode 100644 index 0000000..a91b109 --- /dev/null +++ b/demo/Ursa.Demo/Converters/MenuDataTemplateSelector.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Ursa.Demo.ViewModels; + +namespace Ursa.Demo.Converters; + +public class MenuDataTemplateSelector: IDataTemplate +{ + public IDataTemplate? MenuTemplate { get; set; } + public IDataTemplate? SeparatorTemplate { get; set; } + + public Control? Build(object? param) + { + if (param is NavigationMenuItemViewModel vm) + { + if (vm.IsSeparator) return SeparatorTemplate?.Build(vm); + else return MenuTemplate?.Build(vm); + } + + return null; + } + + public bool Match(object? data) + { + return true; + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml index dad4c0b..414d4de 100644 --- a/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml @@ -21,35 +21,48 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/ViewModels/NavigationMenuDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/NavigationMenuDemoViewModel.cs index 8a394ba..4b7c0c6 100644 --- a/demo/Ursa.Demo/ViewModels/NavigationMenuDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/NavigationMenuDemoViewModel.cs @@ -25,6 +25,11 @@ public class NavigationMenuDemoViewModel: ObservableObject } }, new NavigationMenuItemViewModel() + { + MenuHeader = "附加功能", + IsSeparator = true, + }, + new NavigationMenuItemViewModel() { MenuHeader = "任务平台", MenuIconName = "Gear", @@ -41,5 +46,7 @@ public class NavigationMenuItemViewModel: ObservableObject { public string MenuHeader { get; set; } public string MenuIconName { get; set; } + + public bool IsSeparator { get; set; } public ObservableCollection Children { get; set; } = new(); } \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/Navigation.axaml b/src/Ursa.Themes.Semi/Controls/Navigation.axaml index 6b48847..5318098 100644 --- a/src/Ursa.Themes.Semi/Controls/Navigation.axaml +++ b/src/Ursa.Themes.Semi/Controls/Navigation.axaml @@ -297,4 +297,40 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Ursa/Controls/Navigation/NavigationMenuSeparator.cs b/src/Ursa/Controls/Navigation/NavigationMenuSeparator.cs index ba84405..164d886 100644 --- a/src/Ursa/Controls/Navigation/NavigationMenuSeparator.cs +++ b/src/Ursa/Controls/Navigation/NavigationMenuSeparator.cs @@ -1,6 +1,11 @@ +using Avalonia.Input; + namespace Ursa.Controls; public class NavigationMenuSeparator: NavigationMenuItem { - + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + e.Handled = true; + } } \ No newline at end of file From 6f81fda2cd7c42ad8175622d89a934dbb806612e Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 23 Jun 2023 14:46:52 +0800 Subject: [PATCH 11/11] fix: remove theme scope in demo. --- demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml | 84 +++++++++---------- 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml index 414d4de..8e608a7 100644 --- a/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavigationMenuDemo.axaml @@ -21,48 +21,46 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + +