From 36bb3b563f3d755c558a0483902537278cf38b37 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sun, 11 Feb 2024 23:43:20 +0800 Subject: [PATCH 01/20] WIP: init. --- demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml | 17 +++++++++++ src/Ursa.Themes.Semi/Controls/NavMenu.axaml | 32 +++++++++++++++++++++ src/Ursa.Themes.Semi/Controls/_index.axaml | 1 + src/Ursa/Controls/NavMenu/NavMenu.cs | 17 +++++++++++ src/Ursa/Controls/NavMenu/NavMenuItem.cs | 27 +++++++++++++++++ 5 files changed, 94 insertions(+) create mode 100644 src/Ursa.Themes.Semi/Controls/NavMenu.axaml create mode 100644 src/Ursa/Controls/NavMenu/NavMenu.cs create mode 100644 src/Ursa/Controls/NavMenu/NavMenuItem.cs diff --git a/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml b/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml index 18068cf..23b4f44 100644 --- a/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml +++ b/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml @@ -51,5 +51,22 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml new file mode 100644 index 0000000..a381d61 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index 4bd0c67..0b2dae5 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -18,6 +18,7 @@ + diff --git a/src/Ursa/Controls/NavMenu/NavMenu.cs b/src/Ursa/Controls/NavMenu/NavMenu.cs new file mode 100644 index 0000000..1a06dc3 --- /dev/null +++ b/src/Ursa/Controls/NavMenu/NavMenu.cs @@ -0,0 +1,17 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; + +namespace Ursa.Controls; + +public class NavMenu: SelectingItemsControl +{ + 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(); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/NavMenu/NavMenuItem.cs b/src/Ursa/Controls/NavMenu/NavMenuItem.cs new file mode 100644 index 0000000..860e6f8 --- /dev/null +++ b/src/Ursa/Controls/NavMenu/NavMenuItem.cs @@ -0,0 +1,27 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; + +namespace Ursa.Controls; + +public class NavMenuItem: HeaderedSelectingItemsControl +{ + public static readonly StyledProperty IconProperty = AvaloniaProperty.Register( + nameof(Icon)); + + public object? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + 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(); + } +} \ No newline at end of file From 49fdf80b7bfc7b1d7e518704f5c0296ea9ba5a99 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sun, 11 Feb 2024 23:52:23 +0800 Subject: [PATCH 02/20] feat: try to use grid as panel. --- demo/Ursa.Demo/Models/MenuKeys.cs | 1 + demo/Ursa.Demo/Pages/NavMenuDemo.axaml | 25 +++++++++++++++++++ demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs | 13 ++++++++++ demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml | 17 ------------- .../Ursa.Demo/ViewModels/MainViewViewModel.cs | 1 + demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 1 + .../ViewModels/NavMenuDemoViewModel.cs | 8 ++++++ src/Ursa.Themes.Semi/Controls/NavMenu.axaml | 3 ++- 8 files changed, 51 insertions(+), 18 deletions(-) create mode 100644 demo/Ursa.Demo/Pages/NavMenuDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs create mode 100644 demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs index ea8f8f5..56e6a49 100644 --- a/demo/Ursa.Demo/Models/MenuKeys.cs +++ b/demo/Ursa.Demo/Models/MenuKeys.cs @@ -19,6 +19,7 @@ public static class MenuKeys public const string MenuKeyLoading = "Loading"; public const string MenuKeyMessageBox = "MessageBox"; public const string MenuKeyNavigation = "Navigation"; + public const string MenuKeyNavMenu = "NavMenu"; public const string MenuKeyNumericUpDown = "NumericUpDown"; public const string MenuKeyPagination = "Pagination"; public const string MenuKeyRangeSlider = "RangeSlider"; diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml new file mode 100644 index 0000000..b9d58c6 --- /dev/null +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs new file mode 100644 index 0000000..30898d6 --- /dev/null +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class NavMenuDemo : UserControl +{ + public NavMenuDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml b/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml index 23b4f44..18068cf 100644 --- a/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml +++ b/demo/Ursa.Demo/Pages/SelectionBoxDemo.axaml @@ -51,22 +51,5 @@ - - - - - - - - - - - - - - - - - diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 0a20fe2..43166bd 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -41,6 +41,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyLoading => new LoadingDemoViewModel(), MenuKeys.MenuKeyMessageBox => new MessageBoxDemoViewModel(), MenuKeys.MenuKeyNavigation => new NavigationMenuDemoViewModel(), + MenuKeys.MenuKeyNavMenu => new NavMenuDemoViewModel(), MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(), MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(), MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(), diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index 1377af9..6f6da25 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -28,6 +28,7 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "Loading", Key = MenuKeys.MenuKeyLoading }, new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox, Status = "New" }, new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation, Status = "WIP" }, + new() { MenuHeader = "Nav Menu", Key = MenuKeys.MenuKeyNavMenu, Status = "WIP"}, new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown, Status = "New" }, new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination }, new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider, Status = "New"}, diff --git a/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs new file mode 100644 index 0000000..c99730f --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public class NavMenuDemoViewModel: ObservableObject +{ + +} \ 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 a381d61..a2adf6e 100644 --- a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml +++ b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml @@ -3,6 +3,7 @@ xmlns:u="https://irihi.tech/ursa"> + @@ -20,7 +21,7 @@ - + From 5e5e0844e11128a90f8a7d74ace46a8e72077150 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Mon, 12 Feb 2024 00:10:33 +0800 Subject: [PATCH 03/20] feat: try to setup communication. --- src/Ursa/Controls/NavMenu/NavMenu.cs | 15 +++++++ src/Ursa/Controls/NavMenu/NavMenuItem.cs | 53 ++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/Ursa/Controls/NavMenu/NavMenu.cs b/src/Ursa/Controls/NavMenu/NavMenu.cs index 1a06dc3..4861df5 100644 --- a/src/Ursa/Controls/NavMenu/NavMenu.cs +++ b/src/Ursa/Controls/NavMenu/NavMenu.cs @@ -1,5 +1,6 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.LogicalTree; namespace Ursa.Controls; @@ -14,4 +15,18 @@ public class NavMenu: SelectingItemsControl { return new NavMenuItem(); } + + internal void SelectItem(NavMenuItem item) + { + if (item.IsSelected) return; + var children = this.LogicalChildren.OfType(); + foreach (var child in children) + { + if (child != item) + { + child.IsSelected = false; + } + } + item.IsSelected = true; + } } \ No newline at end of file diff --git a/src/Ursa/Controls/NavMenu/NavMenuItem.cs b/src/Ursa/Controls/NavMenu/NavMenuItem.cs index 860e6f8..8ea4137 100644 --- a/src/Ursa/Controls/NavMenu/NavMenuItem.cs +++ b/src/Ursa/Controls/NavMenu/NavMenuItem.cs @@ -1,11 +1,18 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.VisualTree; namespace Ursa.Controls; public class NavMenuItem: HeaderedSelectingItemsControl { + private NavMenu? _rootMenu; + public static readonly StyledProperty IconProperty = AvaloniaProperty.Register( nameof(Icon)); @@ -14,6 +21,30 @@ public class NavMenuItem: HeaderedSelectingItemsControl 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 new static readonly StyledProperty IsSelectedProperty = + SelectingItemsControl.IsSelectedProperty.AddOwner(); + + public bool IsSelected + { + get => GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); + } + + static NavMenuItem() + { + SelectableMixin.Attach(IsSelectedProperty); + PressedMixin.Attach(); + } protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { @@ -24,4 +55,26 @@ public class NavMenuItem: HeaderedSelectingItemsControl { return new NavMenuItem(); } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _rootMenu = GetRootMenu(); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + _rootMenu?.SelectItem(this); + } + + private NavMenu? GetRootMenu() + { + var root = this.FindAncestorOfType(); + if (root is null) + { + root = this.FindLogicalAncestorOfType(); + } + return root; + } } \ No newline at end of file From 0a3dcf0d8c7f3b00f016eeb56f0a251c5530a5ca Mon Sep 17 00:00:00 2001 From: rabbitism Date: Mon, 12 Feb 2024 00:37:20 +0800 Subject: [PATCH 04/20] feat: deal with selection. --- demo/Ursa.Demo/Pages/NavMenuDemo.axaml | 4 ++- src/Ursa.Themes.Semi/Controls/NavMenu.axaml | 24 +++++++-------- src/Ursa/Controls/NavMenu/NavMenu.cs | 34 +++++++++++++++++++-- src/Ursa/Controls/NavMenu/NavMenuItem.cs | 1 + 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml index b9d58c6..d0f1648 100644 --- a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml @@ -5,7 +5,7 @@ xmlns:u="https://irihi.tech/ursa" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Ursa.Demo.Pages.NavMenuDemo"> - + @@ -21,5 +21,7 @@ + + diff --git a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml index a2adf6e..6843a3c 100644 --- a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml +++ b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml @@ -14,19 +14,17 @@ - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/src/Ursa/Controls/NavMenu/NavMenu.cs b/src/Ursa/Controls/NavMenu/NavMenu.cs index 4861df5..f0b99b6 100644 --- a/src/Ursa/Controls/NavMenu/NavMenu.cs +++ b/src/Ursa/Controls/NavMenu/NavMenu.cs @@ -1,11 +1,33 @@ -using Avalonia.Controls; +using System.Diagnostics; +using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Data; using Avalonia.LogicalTree; namespace Ursa.Controls; -public class NavMenu: SelectingItemsControl +public class NavMenu: ItemsControl { + public static readonly StyledProperty SelectedItemProperty = AvaloniaProperty.Register( + nameof(SelectedItem), defaultBindingMode: BindingMode.TwoWay); + + public object? SelectedItem + { + get => GetValue(SelectedItemProperty); + set => SetValue(SelectedItemProperty, value); + } + + static NavMenu() + { + SelectedItemProperty.Changed.AddClassHandler((o, e) => o.OnSelectedItemChange(e)); + } + + private void OnSelectedItemChange(AvaloniaPropertyChangedEventArgs args) + { + Debug.WriteLine(args.NewValue.Value); + } + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { return NeedsContainer(item, out recycleKey); @@ -27,6 +49,14 @@ public class NavMenu: SelectingItemsControl child.IsSelected = false; } } + if (item.DataContext is not null && item.DataContext != this.DataContext) + { + SelectedItem = item.DataContext; + } + else + { + SelectedItem = item; + } item.IsSelected = true; } } \ No newline at end of file diff --git a/src/Ursa/Controls/NavMenu/NavMenuItem.cs b/src/Ursa/Controls/NavMenu/NavMenuItem.cs index 8ea4137..6195ed9 100644 --- a/src/Ursa/Controls/NavMenu/NavMenuItem.cs +++ b/src/Ursa/Controls/NavMenu/NavMenuItem.cs @@ -60,6 +60,7 @@ public class NavMenuItem: HeaderedSelectingItemsControl { base.OnAttachedToVisualTree(e); _rootMenu = GetRootMenu(); + UpdateSelection(1); } protected override void OnPointerPressed(PointerPressedEventArgs e) From bc9412aad2f3fec3c351dc49ab26a00a4603f2f9 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Mon, 12 Feb 2024 12:12:36 +0800 Subject: [PATCH 05/20] WIP: inherit binding from root. --- demo/Ursa.Demo/Pages/NavMenuDemo.axaml | 79 +++++++++++++------ demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs | 2 + .../ViewModels/NavMenuDemoViewModel.cs | 45 ++++++++++- src/Ursa/Controls/NavMenu/NavMenu.cs | 56 ++++++++++++- src/Ursa/Controls/NavMenu/NavMenuItem.cs | 38 +++++++-- 5 files changed, 184 insertions(+), 36 deletions(-) diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml index d0f1648..984bea3 100644 --- a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml @@ -1,27 +1,54 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs index 30898d6..635ab59 100644 --- a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Ursa.Demo.ViewModels; namespace Ursa.Demo.Pages; @@ -9,5 +10,6 @@ public partial class NavMenuDemo : UserControl public NavMenuDemo() { InitializeComponent(); + this.DataContext = new NavMenuDemoViewModel(); } } \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs index c99730f..aea2ff2 100644 --- a/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs @@ -1,8 +1,49 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; namespace Ursa.Demo.ViewModels; public class NavMenuDemoViewModel: ObservableObject { - + public ObservableCollection MenuItems { get; set; } = new ObservableCollection + { + new MenuItem { Header = "Introduction" , Children = + { + new MenuItem() { Header = "Getting Started" }, + new MenuItem() { Header = "Design Principles" }, + new MenuItem() { Header = "Contributing" }, + }}, + new MenuItem { Header = "Badge" }, + new MenuItem { Header = "Banner" }, + new MenuItem { Header = "ButtonGroup" }, + new MenuItem { Header = "Class Input" }, + new MenuItem { Header = "Dialog" }, + new MenuItem { Header = "Divider" }, + new MenuItem { Header = "Drawer" }, + new MenuItem { Header = "DualBadge" }, + new MenuItem { Header = "EnumSelector" }, + new MenuItem { Header = "ImageViewer" }, + new MenuItem { Header = "IPv4Box" }, + new MenuItem { Header = "IconButton" }, + new MenuItem { Header = "KeyGestureInput" }, + new MenuItem { Header = "Loading" }, + new MenuItem { Header = "MessageBox" }, + new MenuItem { Header = "Navigation" }, + new MenuItem { Header = "NavMenu" }, + new MenuItem { Header = "NumericUpDown" }, + new MenuItem { Header = "Pagination" }, + new MenuItem { Header = "RangeSlider" }, + new MenuItem { Header = "SelectionList" }, + new MenuItem { Header = "TagInput" }, + new MenuItem { Header = "Timeline" }, + new MenuItem { Header = "TwoTonePathIcon" }, + new MenuItem { Header = "ThemeToggler" } + }; +} + +public class MenuItem +{ + public string? Header { get; set; } + public string? Icon { get; set; } + public ObservableCollection Children { get; set; } = new ObservableCollection(); } \ No newline at end of file diff --git a/src/Ursa/Controls/NavMenu/NavMenu.cs b/src/Ursa/Controls/NavMenu/NavMenu.cs index f0b99b6..7469e42 100644 --- a/src/Ursa/Controls/NavMenu/NavMenu.cs +++ b/src/Ursa/Controls/NavMenu/NavMenu.cs @@ -4,6 +4,7 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.LogicalTree; +using Avalonia.Metadata; namespace Ursa.Controls; @@ -18,6 +19,39 @@ public class NavMenu: ItemsControl set => SetValue(SelectedItemProperty, value); } + public static readonly StyledProperty IconBindingProperty = AvaloniaProperty.Register( + nameof(IconBinding)); + + [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IBinding? IconBinding + { + get => GetValue(IconBindingProperty); + set => SetValue(IconBindingProperty, value); + } + + public static readonly StyledProperty HeaderBindingProperty = AvaloniaProperty.Register( + nameof(HeaderBinding)); + + [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IBinding? HeaderBinding + { + get => GetValue(HeaderBindingProperty); + set => SetValue(HeaderBindingProperty, value); + } + + public static readonly StyledProperty SubMenuBindingProperty = AvaloniaProperty.Register( + nameof(SubMenuBinding)); + + [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IBinding? SubMenuBinding + { + get => GetValue(SubMenuBindingProperty); + set => SetValue(SubMenuBindingProperty, value); + } + static NavMenu() { SelectedItemProperty.Changed.AddClassHandler((o, e) => o.OnSelectedItemChange(e)); @@ -37,7 +71,27 @@ public class NavMenu: ItemsControl { return new NavMenuItem(); } - + + protected override void PrepareContainerForItemOverride(Control container, object? item, int index) + { + base.PrepareContainerForItemOverride(container, item, index); + if (container is NavMenuItem navMenuItem) + { + if (IconBinding is not null) + { + navMenuItem[!NavMenuItem.IconProperty] = IconBinding; + } + if (HeaderBinding is not null) + { + navMenuItem[!HeaderedItemsControl.HeaderProperty] = HeaderBinding; + } + if (SubMenuBinding is not null) + { + navMenuItem[!ItemsSourceProperty] = SubMenuBinding; + } + } + } + internal void SelectItem(NavMenuItem item) { if (item.IsSelected) return; diff --git a/src/Ursa/Controls/NavMenu/NavMenuItem.cs b/src/Ursa/Controls/NavMenu/NavMenuItem.cs index 6195ed9..ccd9ffd 100644 --- a/src/Ursa/Controls/NavMenu/NavMenuItem.cs +++ b/src/Ursa/Controls/NavMenu/NavMenuItem.cs @@ -1,5 +1,7 @@ -using Avalonia; +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; @@ -9,8 +11,11 @@ using Avalonia.VisualTree; namespace Ursa.Controls; +[PseudoClasses(PC_Highlighted)] public class NavMenuItem: HeaderedSelectingItemsControl { + public const string PC_Highlighted = "highlighted"; + private NavMenu? _rootMenu; public static readonly StyledProperty IconProperty = AvaloniaProperty.Register( @@ -31,6 +36,15 @@ public class NavMenuItem: HeaderedSelectingItemsControl set => SetValue(IconTemplateProperty, value); } + public static readonly StyledProperty CommandProperty = AvaloniaProperty.Register( + nameof(Command)); + + public ICommand? Command + { + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + public new static readonly StyledProperty IsSelectedProperty = SelectingItemsControl.IsSelectedProperty.AddOwner(); @@ -39,6 +53,8 @@ public class NavMenuItem: HeaderedSelectingItemsControl get => GetValue(IsSelectedProperty); set => SetValue(IsSelectedProperty, value); } + + static NavMenuItem() { @@ -56,26 +72,34 @@ public class NavMenuItem: HeaderedSelectingItemsControl return new NavMenuItem(); } + protected override void PrepareContainerForItemOverride(Control container, object? item, int index) + { + base.PrepareContainerForItemOverride(container, item, index); + if (container is NavMenuItem navMenuItem) + { + if (_rootMenu?.HeaderBinding is not null) + { + container[!HeaderProperty] = _rootMenu.HeaderBinding; + } + } + } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); _rootMenu = GetRootMenu(); - UpdateSelection(1); } protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); _rootMenu?.SelectItem(this); + e.Handled = true; } private NavMenu? GetRootMenu() { - var root = this.FindAncestorOfType(); - if (root is null) - { - root = this.FindLogicalAncestorOfType(); - } + var root = this.FindAncestorOfType() ?? this.FindLogicalAncestorOfType(); return root; } } \ No newline at end of file From f9802d222b1f48a10380266c7a6063236c51abf0 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Mon, 12 Feb 2024 15:49:01 +0800 Subject: [PATCH 06/20] feat: add pseudo classes. --- demo/Ursa.Demo/Pages/NavMenuDemo.axaml | 4 +- .../ViewModels/NavMenuDemoViewModel.cs | 23 +++- src/Ursa.Themes.Semi/Controls/NavMenu.axaml | 35 ++++-- src/Ursa/Controls/NavMenu/NavMenu.cs | 33 ++++- src/Ursa/Controls/NavMenu/NavMenuItem.cs | 116 +++++++++++++++++- 5 files changed, 185 insertions(+), 26 deletions(-) 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 From dc3c61dab5d293e3fa604e2d4d750959f3095dbc Mon Sep 17 00:00:00 2001 From: rabbitism Date: Mon, 12 Feb 2024 20:47:55 +0800 Subject: [PATCH 07/20] wip. --- demo/Ursa.Demo/Pages/NavMenuDemo.axaml | 84 +++++++++---------- .../ViewModels/NavMenuDemoViewModel.cs | 7 +- src/Ursa.Themes.Semi/Controls/NavMenu.axaml | 19 +++-- src/Ursa/Controls/NavMenu/NavMenuItem.cs | 19 ++++- 4 files changed, 80 insertions(+), 49 deletions(-) diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml index e9760a2..05edc59 100644 --- a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml @@ -11,46 +11,46 @@ x:CompileBindings="True" x:DataType="vm:NavMenuDemoViewModel" mc:Ignorable="d"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs index 6aa42ac..f7dc981 100644 --- a/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs @@ -13,7 +13,12 @@ public class NavMenuDemoViewModel: ObservableObject { new MenuItem { Header = "Introduction" , Children = { - new MenuItem() { Header = "Getting Started" }, + new MenuItem() { Header = "Getting Started", Children = + { + new MenuItem() { Header = "Code of Conduct" }, + new MenuItem() { Header = "How to Contribute" }, + new MenuItem() { Header = "Development Workflow" }, + }}, new MenuItem() { Header = "Design Principles" }, new MenuItem() { Header = "Contributing", Children = { diff --git a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml index 918bdc1..2f24300 100644 --- a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml +++ b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml @@ -7,7 +7,9 @@ - + + + @@ -15,19 +17,22 @@ - + - + + + + diff --git a/src/Ursa/Controls/NavMenu/NavMenuItem.cs b/src/Ursa/Controls/NavMenu/NavMenuItem.cs index 6424278..da05dcd 100644 --- a/src/Ursa/Controls/NavMenu/NavMenuItem.cs +++ b/src/Ursa/Controls/NavMenu/NavMenuItem.cs @@ -11,6 +11,12 @@ using Avalonia.VisualTree; namespace Ursa.Controls; +/// +/// Navigation Menu Item +/// Note: +/// collapsed: Entire menu is collapsed, only first level icon is displayed. Submenus are in popup. +/// closed: When menu is not in collapsed mode, represents whether submenu is hidden. +/// [PseudoClasses(PC_Highlighted, PC_Collapsed, PC_Closed, PC_FirstLevel, PC_Selector)] public class NavMenuItem: HeaderedSelectingItemsControl { @@ -145,7 +151,10 @@ public class NavMenuItem: HeaderedSelectingItemsControl protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); - SelectItem(this); + if (this.ItemCount == 0) + { + SelectItem(this); + } Command?.Execute(CommandParameter); e.Handled = true; } @@ -165,6 +174,14 @@ public class NavMenuItem: HeaderedSelectingItemsControl 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) { From c6f440bd56b929a3f293ec790084d8a5b4896d4f Mon Sep 17 00:00:00 2001 From: rabbitism Date: Mon, 12 Feb 2024 21:42:05 +0800 Subject: [PATCH 08/20] wip: simplify container preparation. --- demo/Ursa.Demo/Pages/NavMenuDemo.axaml | 10 ++- src/Ursa.Themes.Semi/Controls/NavMenu.axaml | 13 ++-- src/Ursa/Controls/NavMenu/NavMenu.cs | 50 ++++++-------- src/Ursa/Controls/NavMenu/NavMenuItem.cs | 74 ++++++++++++++------- 4 files changed, 85 insertions(+), 62 deletions(-) diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml index 05edc59..0b192bb 100644 --- a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml @@ -48,9 +48,15 @@ + IconBinding="{Binding}"> + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml index 2f24300..9a98a0d 100644 --- a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml +++ b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml @@ -17,7 +17,7 @@ - + @@ -25,15 +25,16 @@ + Content="{TemplateBinding Icon}" + ContentTemplate="{TemplateBinding IconTemplate}" /> + Content="{TemplateBinding Header}" ContentTemplate="{TemplateBinding HeaderTemplate}" /> - + diff --git a/src/Ursa/Controls/NavMenu/NavMenu.cs b/src/Ursa/Controls/NavMenu/NavMenu.cs index 38baf33..851dbb2 100644 --- a/src/Ursa/Controls/NavMenu/NavMenu.cs +++ b/src/Ursa/Controls/NavMenu/NavMenu.cs @@ -2,6 +2,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.LogicalTree; using Avalonia.Metadata; @@ -63,6 +64,24 @@ public class NavMenu: ItemsControl set => SetValue(CommandBindingProperty, value); } + public static readonly StyledProperty HeaderTemplateProperty = AvaloniaProperty.Register( + nameof(HeaderTemplate)); + + public IDataTemplate? HeaderTemplate + { + get => GetValue(HeaderTemplateProperty); + set => SetValue(HeaderTemplateProperty, value); + } + + public static readonly StyledProperty IconTemplateProperty = AvaloniaProperty.Register( + nameof(IconTemplate)); + + public IDataTemplate? IconTemplate + { + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); + } + static NavMenu() { SelectedItemProperty.Changed.AddClassHandler((o, e) => o.OnSelectedItemChange(e)); @@ -83,30 +102,6 @@ public class NavMenu: ItemsControl return new NavMenuItem(); } - protected override void PrepareContainerForItemOverride(Control container, object? item, int index) - { - base.PrepareContainerForItemOverride(container, item, index); - if (container is NavMenuItem navMenuItem) - { - if (IconBinding is not null) - { - navMenuItem[!NavMenuItem.IconProperty] = IconBinding; - } - if (HeaderBinding is not null) - { - navMenuItem[!HeaderedItemsControl.HeaderProperty] = HeaderBinding; - } - if (SubMenuBinding is not null) - { - navMenuItem[!ItemsSourceProperty] = SubMenuBinding; - } - if (CommandBinding is not null) - { - navMenuItem[!NavMenuItem.CommandProperty] = CommandBinding; - } - } - } - internal void SelectItem(NavMenuItem item, NavMenuItem parent) { // if (item.IsSelected) return; @@ -116,12 +111,9 @@ public class NavMenu: ItemsControl { continue; } - else + if (child is NavMenuItem navMenuItem) { - if (child is NavMenuItem navMenuItem) - { - navMenuItem.ClearSelection(); - } + 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 da05dcd..8737ab8 100644 --- a/src/Ursa/Controls/NavMenu/NavMenuItem.cs +++ b/src/Ursa/Controls/NavMenu/NavMenuItem.cs @@ -83,6 +83,28 @@ public class NavMenuItem: HeaderedSelectingItemsControl get => _isHighlighted; private set => SetAndRaise(IsHighlightedProperty, ref _isHighlighted, value); } + + private bool _isCollapsed; + + public static readonly DirectProperty IsCollapsedProperty = AvaloniaProperty.RegisterDirect( + nameof(IsCollapsed), o => o.IsCollapsed, (o, v) => o.IsCollapsed = v); + + public bool IsCollapsed + { + get => _isCollapsed; + set => SetAndRaise(IsCollapsedProperty, ref _isCollapsed, value); + } + + private bool _isClosed; + + public static readonly DirectProperty IsClosedProperty = AvaloniaProperty.RegisterDirect( + nameof(IsClosed), o => o.IsClosed, (o, v) => o.IsClosed = v); + + public bool IsClosed + { + get => _isClosed; + set => SetAndRaise(IsClosedProperty, ref _isClosed, value); + } internal int Level { get; set; } @@ -109,35 +131,33 @@ public class NavMenuItem: HeaderedSelectingItemsControl { return new NavMenuItem(); } - - protected override void PrepareContainerForItemOverride(Control container, object? item, int index) - { - base.PrepareContainerForItemOverride(container, item, index); - if (container is NavMenuItem navMenuItem) - { - if (_rootMenu?.HeaderBinding is not null) - { - 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; - } - } - } - + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); _rootMenu = GetRootMenu(); + if (_rootMenu is not null) + { + if (_rootMenu.IconBinding is not null) + { + this[!IconProperty] = _rootMenu.IconBinding; + } + if (_rootMenu.HeaderBinding is not null) + { + this[!HeaderProperty] = _rootMenu.HeaderBinding; + } + if (_rootMenu.SubMenuBinding is not null) + { + this[!ItemsSourceProperty] = _rootMenu.SubMenuBinding; + } + if (_rootMenu.CommandBinding is not null) + { + this[!CommandProperty] = _rootMenu.CommandBinding; + } + this[!IconTemplateProperty] = _rootMenu[!NavMenu.IconTemplateProperty]; + this[!HeaderTemplateProperty] = _rootMenu[!NavMenu.HeaderTemplateProperty]; + } + } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) @@ -154,6 +174,10 @@ public class NavMenuItem: HeaderedSelectingItemsControl if (this.ItemCount == 0) { SelectItem(this); + } + else + { + } Command?.Execute(CommandParameter); e.Handled = true; From cff3cdbf981f463157c8ad8b0a7b12b9de1831d7 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 13 Feb 2024 13:51:02 +0800 Subject: [PATCH 09/20] feat: update dependency. --- src/Ursa/Controls/NavMenu/NavMenu.cs | 24 ++++++++++++ src/Ursa/Controls/NavMenu/NavMenuItem.cs | 47 +++++++++++------------- src/Ursa/Ursa.csproj | 2 +- 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/Ursa/Controls/NavMenu/NavMenu.cs b/src/Ursa/Controls/NavMenu/NavMenu.cs index 851dbb2..9e4d41a 100644 --- a/src/Ursa/Controls/NavMenu/NavMenu.cs +++ b/src/Ursa/Controls/NavMenu/NavMenu.cs @@ -1,16 +1,21 @@ using System.Diagnostics; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.LogicalTree; using Avalonia.Metadata; +using Irihi.Avalonia.Shared.Helpers; namespace Ursa.Controls; +[PseudoClasses(PC_HorizontalCollapsed)] public class NavMenu: ItemsControl { + public const string PC_HorizontalCollapsed = ":horizontal-collapsed"; + public static readonly StyledProperty SelectedItemProperty = AvaloniaProperty.Register( nameof(SelectedItem), defaultBindingMode: BindingMode.TwoWay); @@ -82,9 +87,28 @@ public class NavMenu: ItemsControl set => SetValue(IconTemplateProperty, value); } + public static readonly StyledProperty SubMenuIndentProperty = AvaloniaProperty.Register( + nameof(SubMenuIndent), defaultValue: 20.0); + + public double SubMenuIndent + { + get => GetValue(SubMenuIndentProperty); + set => SetValue(SubMenuIndentProperty, value); + } + + public static readonly StyledProperty IsHorizontalCollapsedProperty = AvaloniaProperty.Register( + nameof(IsHorizontalCollapsed)); + + public bool IsHorizontalCollapsed + { + get => GetValue(IsHorizontalCollapsedProperty); + set => SetValue(IsHorizontalCollapsedProperty, value); + } + static NavMenu() { SelectedItemProperty.Changed.AddClassHandler((o, e) => o.OnSelectedItemChange(e)); + PropertyToPseudoClassMixin.Attach(IsHorizontalCollapsedProperty, PC_HorizontalCollapsed); } private void OnSelectedItemChange(AvaloniaPropertyChangedEventArgs args) diff --git a/src/Ursa/Controls/NavMenu/NavMenuItem.cs b/src/Ursa/Controls/NavMenu/NavMenuItem.cs index 8737ab8..b5a1baf 100644 --- a/src/Ursa/Controls/NavMenu/NavMenuItem.cs +++ b/src/Ursa/Controls/NavMenu/NavMenuItem.cs @@ -13,17 +13,14 @@ namespace Ursa.Controls; /// /// Navigation Menu Item -/// Note: -/// collapsed: Entire menu is collapsed, only first level icon is displayed. Submenus are in popup. -/// closed: When menu is not in collapsed mode, represents whether submenu is hidden. /// -[PseudoClasses(PC_Highlighted, PC_Collapsed, PC_Closed, PC_FirstLevel, PC_Selector)] +[PseudoClasses(PC_Highlighted, PC_HorizontalCollapsed, PC_VerticalCollapsed, PC_FirstLevel, PC_Selector)] public class NavMenuItem: HeaderedSelectingItemsControl { 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_HorizontalCollapsed = ":horizontal-collapsed"; + public const string PC_VerticalCollapsed = ":vertical-collapsed"; public const string PC_Selector = ":selector"; private NavMenu? _rootMenu; @@ -84,37 +81,38 @@ public class NavMenuItem: HeaderedSelectingItemsControl private set => SetAndRaise(IsHighlightedProperty, ref _isHighlighted, value); } - private bool _isCollapsed; + public static readonly StyledProperty SubMenuIndentProperty = + NavMenu.SubMenuIndentProperty.AddOwner(); - public static readonly DirectProperty IsCollapsedProperty = AvaloniaProperty.RegisterDirect( - nameof(IsCollapsed), o => o.IsCollapsed, (o, v) => o.IsCollapsed = v); - - public bool IsCollapsed + public double SubMenuIndent { - get => _isCollapsed; - set => SetAndRaise(IsCollapsedProperty, ref _isCollapsed, value); + get => GetValue(SubMenuIndentProperty); + set => SetValue(SubMenuIndentProperty, value); } - private bool _isClosed; + - public static readonly DirectProperty IsClosedProperty = AvaloniaProperty.RegisterDirect( - nameof(IsClosed), o => o.IsClosed, (o, v) => o.IsClosed = v); - - public bool IsClosed + internal static readonly DirectProperty LevelProperty = AvaloniaProperty.RegisterDirect( + nameof(Level), o => o.Level, (o, v) => o.Level = v); + private int _level; + internal int Level { - get => _isClosed; - set => SetAndRaise(IsClosedProperty, ref _isClosed, value); + get => _level; + set => SetAndRaise(LevelProperty, ref _level, value); } - - internal int Level { get; set; } - static NavMenuItem() { SelectableMixin.Attach(IsSelectedProperty); PressedMixin.Attach(); IsHighlightedProperty.Changed.AddClassHandler((o, e) => o.OnIsHighlightedChange(e)); + LevelProperty.Changed.AddClassHandler((item, args) => item.OnLevelChange(args)); + } + + private void OnLevelChange(AvaloniaPropertyChangedEventArgs args) + { + PseudoClasses.Set(PC_FirstLevel, args.NewValue.Value == 0); } private void OnIsHighlightedChange(AvaloniaPropertyChangedEventArgs args) @@ -164,8 +162,7 @@ public class NavMenuItem: HeaderedSelectingItemsControl { var children = this.ItemsPanelRoot?.Children.ToList(); base.OnApplyTemplate(e); - Level = CalculateDistanceFromLogicalParent(this); - PseudoClasses.Set(PC_FirstLevel, Level == 0); + SetCurrentValue(LevelProperty,CalculateDistanceFromLogicalParent(this)); } protected override void OnPointerPressed(PointerPressedEventArgs e) diff --git a/src/Ursa/Ursa.csproj b/src/Ursa/Ursa.csproj index a0a7163..6057345 100644 --- a/src/Ursa/Ursa.csproj +++ b/src/Ursa/Ursa.csproj @@ -14,7 +14,7 @@ - + From e227788a9589f065677622dc30576dc9d23dfea4 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 13 Feb 2024 16:09:53 +0800 Subject: [PATCH 10/20] feat: add more templates. --- demo/Ursa.Demo/Pages/NavMenuDemo.axaml | 7 +- src/Ursa.Themes.Semi/Controls/NavMenu.axaml | 186 +++++++++++++++--- .../Converters/NavMenuMarginConverter.cs | 17 ++ src/Ursa/Controls/NavMenu/NavMenu.cs | 2 +- src/Ursa/Controls/NavMenu/NavMenuItem.cs | 58 ++++-- 5 files changed, 226 insertions(+), 44 deletions(-) create mode 100644 src/Ursa.Themes.Semi/Converters/NavMenuMarginConverter.cs diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml index 0b192bb..bf1a6f8 100644 --- a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml @@ -12,9 +12,9 @@ x:DataType="vm:NavMenuDemoViewModel" mc:Ignorable="d"> - + - + + Collapse + + + + @@ -14,39 +19,160 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Converters/NavMenuMarginConverter.cs b/src/Ursa.Themes.Semi/Converters/NavMenuMarginConverter.cs new file mode 100644 index 0000000..999c0b1 --- /dev/null +++ b/src/Ursa.Themes.Semi/Converters/NavMenuMarginConverter.cs @@ -0,0 +1,17 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace Ursa.Themes.Semi.Converters; + +public class NavMenuMarginConverter: IMultiValueConverter +{ + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + if (values[0] is double indent && values[1] is int level) + { + return new Thickness(indent * level, 0, 0, 0); + } + return AvaloniaProperty.UnsetValue; + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/NavMenu/NavMenu.cs b/src/Ursa/Controls/NavMenu/NavMenu.cs index 9e4d41a..d675d0c 100644 --- a/src/Ursa/Controls/NavMenu/NavMenu.cs +++ b/src/Ursa/Controls/NavMenu/NavMenu.cs @@ -88,7 +88,7 @@ public class NavMenu: ItemsControl } public static readonly StyledProperty SubMenuIndentProperty = AvaloniaProperty.Register( - nameof(SubMenuIndent), defaultValue: 20.0); + nameof(SubMenuIndent)); public double SubMenuIndent { diff --git a/src/Ursa/Controls/NavMenu/NavMenuItem.cs b/src/Ursa/Controls/NavMenu/NavMenuItem.cs index b5a1baf..7271c69 100644 --- a/src/Ursa/Controls/NavMenu/NavMenuItem.cs +++ b/src/Ursa/Controls/NavMenu/NavMenuItem.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.VisualTree; +using Irihi.Avalonia.Shared.Helpers; namespace Ursa.Controls; @@ -25,6 +26,7 @@ public class NavMenuItem: HeaderedSelectingItemsControl private NavMenu? _rootMenu; private Panel? _popupPanel; + private Popup? _popup; public static readonly StyledProperty IconProperty = AvaloniaProperty.Register( nameof(Icon)); @@ -81,6 +83,24 @@ public class NavMenuItem: HeaderedSelectingItemsControl 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(); @@ -90,8 +110,6 @@ public class NavMenuItem: HeaderedSelectingItemsControl set => SetValue(SubMenuIndentProperty, value); } - - internal static readonly DirectProperty LevelProperty = AvaloniaProperty.RegisterDirect( nameof(Level), o => o.Level, (o, v) => o.Level = v); private int _level; @@ -104,20 +122,18 @@ public class NavMenuItem: HeaderedSelectingItemsControl static NavMenuItem() { - SelectableMixin.Attach(IsSelectedProperty); + // SelectableMixin.Attach(IsSelectedProperty); PressedMixin.Attach(); - IsHighlightedProperty.Changed.AddClassHandler((o, e) => o.OnIsHighlightedChange(e)); LevelProperty.Changed.AddClassHandler((item, args) => item.OnLevelChange(args)); + PropertyToPseudoClassMixin.Attach(IsHighlightedProperty, PC_Highlighted); + PropertyToPseudoClassMixin.Attach(IsHorizontalCollapsedProperty, PC_HorizontalCollapsed); + PropertyToPseudoClassMixin.Attach(IsVerticalCollapsedProperty, PC_VerticalCollapsed); + PropertyToPseudoClassMixin.Attach(IsSelectedProperty, ":selected", IsSelectedChangedEvent); } private void OnLevelChange(AvaloniaPropertyChangedEventArgs args) { - PseudoClasses.Set(PC_FirstLevel, args.NewValue.Value == 0); - } - - private void OnIsHighlightedChange(AvaloniaPropertyChangedEventArgs args) - { - PseudoClasses.Set(PC_Highlighted, args.NewValue.Value); + PseudoClasses.Set(PC_FirstLevel, args.NewValue.Value == 1); } protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) @@ -154,6 +170,8 @@ public class NavMenuItem: HeaderedSelectingItemsControl } this[!IconTemplateProperty] = _rootMenu[!NavMenu.IconTemplateProperty]; this[!HeaderTemplateProperty] = _rootMenu[!NavMenu.HeaderTemplateProperty]; + this[!SubMenuIndentProperty] = _rootMenu[!NavMenu.SubMenuIndentProperty]; + this[!IsHorizontalCollapsedProperty] = _rootMenu[!NavMenu.IsHorizontalCollapsedProperty]; } } @@ -163,6 +181,7 @@ public class NavMenuItem: HeaderedSelectingItemsControl var children = this.ItemsPanelRoot?.Children.ToList(); base.OnApplyTemplate(e); SetCurrentValue(LevelProperty,CalculateDistanceFromLogicalParent(this)); + _popup = e.NameScope.Find("PART_Popup"); } protected override void OnPointerPressed(PointerPressedEventArgs e) @@ -174,7 +193,24 @@ public class NavMenuItem: HeaderedSelectingItemsControl } else { - + if (!IsHorizontalCollapsed) + { + SetCurrentValue(IsVerticalCollapsedProperty, !IsVerticalCollapsed); + } + else + { + if (_popup is not null) + { + if (_popup.IsOpen) + { + _popup.Close(); + } + else + { + _popup.Open(); + } + } + } } Command?.Execute(CommandParameter); e.Handled = true; From 89e8c2c5a378507069ce232322cbe0e6d8af9592 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 13 Feb 2024 17:47:54 +0800 Subject: [PATCH 11/20] feat: default styling. --- demo/Ursa.Demo/Pages/NavMenuDemo.axaml | 13 +++- .../ViewModels/NavMenuDemoViewModel.cs | 1 - src/Ursa.Themes.Semi/Controls/NavMenu.axaml | 73 ++++++++++++------- .../Converters/NavMenuMarginConverter.cs | 2 +- 4 files changed, 61 insertions(+), 28 deletions(-) diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml index bf1a6f8..59ade47 100644 --- a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml @@ -54,9 +54,20 @@ HeaderBinding="{Binding Header}" SubMenuBinding="{Binding Children}" IconBinding="{Binding}"> + + + + - + + diff --git a/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs index f7dc981..7748926 100644 --- a/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs @@ -4,7 +4,6 @@ using System.Windows.Input; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Ursa.Controls; - namespace Ursa.Demo.ViewModels; public class NavMenuDemoViewModel: ObservableObject diff --git a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml index 3b2e7ca..8236e96 100644 --- a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml +++ b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml @@ -9,7 +9,7 @@ - + @@ -22,8 +22,9 @@ @@ -81,8 +82,11 @@ - + - @@ -132,46 +137,64 @@ Width="8" Height="8" Margin="12,0" + RenderTransform="rotate(-90deg)" Data="{DynamicResource NavigationMenuItemExpandIconGlyph}" Foreground="{DynamicResource NavigationMenuItemExpandIconForeground}"> - - - - - - + - - - - - - + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Converters/NavMenuMarginConverter.cs b/src/Ursa.Themes.Semi/Converters/NavMenuMarginConverter.cs index 999c0b1..6bd2c37 100644 --- a/src/Ursa.Themes.Semi/Converters/NavMenuMarginConverter.cs +++ b/src/Ursa.Themes.Semi/Converters/NavMenuMarginConverter.cs @@ -10,7 +10,7 @@ public class NavMenuMarginConverter: IMultiValueConverter { if (values[0] is double indent && values[1] is int level) { - return new Thickness(indent * level, 0, 0, 0); + return new Thickness(indent * (level-1), 0, 0, 0); } return AvaloniaProperty.UnsetValue; } From a302081ef690606567bdcc35d421cc78be099885 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 13 Feb 2024 20:02:02 +0800 Subject: [PATCH 12/20] feat: temp solution for highlight. --- src/Ursa/Controls/NavMenu/NavMenuItem.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Ursa/Controls/NavMenu/NavMenuItem.cs b/src/Ursa/Controls/NavMenu/NavMenuItem.cs index 7271c69..20cda43 100644 --- a/src/Ursa/Controls/NavMenu/NavMenuItem.cs +++ b/src/Ursa/Controls/NavMenu/NavMenuItem.cs @@ -172,6 +172,10 @@ public class NavMenuItem: HeaderedSelectingItemsControl this[!HeaderTemplateProperty] = _rootMenu[!NavMenu.HeaderTemplateProperty]; this[!SubMenuIndentProperty] = _rootMenu[!NavMenu.SubMenuIndentProperty]; this[!IsHorizontalCollapsedProperty] = _rootMenu[!NavMenu.IsHorizontalCollapsedProperty]; + if (this == _rootMenu.SelectedItem || this.DataContext == _rootMenu.SelectedItem) + { + SelectItem(this); + } } } From 705152104030ee53c365f09f97fefbbeb0487490 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Tue, 13 Feb 2024 22:51:38 +0800 Subject: [PATCH 13/20] feat: resolve highlight binding issue. --- demo/Ursa.Demo/Pages/NavMenuDemo.axaml | 18 ++-- demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs | 12 +++ src/Ursa.Themes.Semi/Controls/NavMenu.axaml | 82 +++++++++++-------- src/Ursa/Controls/NavMenu/NavMenuItem.cs | 67 ++++++++++----- .../Controls/NavMenu/OverflowStackPanel.cs | 30 +++++++ 5 files changed, 145 insertions(+), 64 deletions(-) create mode 100644 src/Ursa/Controls/NavMenu/OverflowStackPanel.cs diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml index 59ade47..5a944cf 100644 --- a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml @@ -13,6 +13,7 @@ mc:Ignorable="d"> + Collapse + + SubMenuBinding="{Binding Children}"> + Width="10" + Height="10" + Classes.Active="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=u:NavMenuItem}, Path=IsHighlighted, Mode=TwoWay}"> diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs index 635ab59..eac27af 100644 --- a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml.cs @@ -1,5 +1,8 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Input; +using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Ursa.Demo.ViewModels; @@ -12,4 +15,13 @@ public partial class NavMenuDemo : UserControl InitializeComponent(); this.DataContext = new NavMenuDemoViewModel(); } + + private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (sender is Rectangle c) + { + c.ApplyStyling(); + var ancestors = c.GetLogicalAncestors(); + } + } } \ 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 8236e96..0909f21 100644 --- a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml +++ b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml @@ -26,6 +26,13 @@ Grid.Row="0" Background="{TemplateBinding u:NavMenuItem.Background}" CornerRadius="4"> + + + + + + + @@ -66,54 +73,47 @@ - - - + Grid.IsSharedSizeScope="True" + ItemsPanel="{Binding ItemsPanel, RelativeSource={RelativeSource TemplatedParent}}" + RenderTransformOrigin="0.5,0" > + - + + - - + + + CornerRadius="4"> - - - - - - + CornerRadius="4"> + @@ -137,16 +137,19 @@ Width="8" Height="8" Margin="12,0" - RenderTransform="rotate(-90deg)" Data="{DynamicResource NavigationMenuItemExpandIconGlyph}" - Foreground="{DynamicResource NavigationMenuItemExpandIconForeground}"> - - + - + @@ -154,14 +157,20 @@ - + + + + + + - - + diff --git a/src/Ursa/Controls/NavMenu/NavMenuItem.cs b/src/Ursa/Controls/NavMenu/NavMenuItem.cs index 20cda43..870ccff 100644 --- a/src/Ursa/Controls/NavMenu/NavMenuItem.cs +++ b/src/Ursa/Controls/NavMenu/NavMenuItem.cs @@ -5,7 +5,9 @@ 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.Helpers; @@ -27,6 +29,8 @@ public class NavMenuItem: HeaderedSelectingItemsControl private NavMenu? _rootMenu; private Panel? _popupPanel; private Popup? _popup; + private Panel? _overflowPanel; + private Border? _border; public static readonly StyledProperty IconProperty = AvaloniaProperty.Register( nameof(Icon)); @@ -74,8 +78,10 @@ public class NavMenuItem: HeaderedSelectingItemsControl private bool _isHighlighted; - public static readonly DirectProperty IsHighlightedProperty = AvaloniaProperty.RegisterDirect( - nameof(IsHighlighted), o => o.IsHighlighted, (o, v) => o.IsHighlighted = v); + public static readonly DirectProperty IsHighlightedProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsHighlighted), o => o.IsHighlighted, (o, v) => o.IsHighlighted = v, + defaultBindingMode: BindingMode.TwoWay); public bool IsHighlighted { @@ -129,6 +135,26 @@ public class NavMenuItem: HeaderedSelectingItemsControl PropertyToPseudoClassMixin.Attach(IsHorizontalCollapsedProperty, PC_HorizontalCollapsed); PropertyToPseudoClassMixin.Attach(IsVerticalCollapsedProperty, PC_VerticalCollapsed); PropertyToPseudoClassMixin.Attach(IsSelectedProperty, ":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) @@ -150,6 +176,15 @@ public class NavMenuItem: HeaderedSelectingItemsControl { 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"); + _border = e.NameScope.Find("PART_Border"); if (_rootMenu is not null) { if (_rootMenu.IconBinding is not null) @@ -172,20 +207,17 @@ public class NavMenuItem: HeaderedSelectingItemsControl this[!HeaderTemplateProperty] = _rootMenu[!NavMenu.HeaderTemplateProperty]; this[!SubMenuIndentProperty] = _rootMenu[!NavMenu.SubMenuIndentProperty]; this[!IsHorizontalCollapsedProperty] = _rootMenu[!NavMenu.IsHorizontalCollapsedProperty]; - if (this == _rootMenu.SelectedItem || this.DataContext == _rootMenu.SelectedItem) - { - SelectItem(this); - } } - } - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + protected override void OnLoaded(RoutedEventArgs e) { - var children = this.ItemsPanelRoot?.Children.ToList(); - base.OnApplyTemplate(e); - SetCurrentValue(LevelProperty,CalculateDistanceFromLogicalParent(this)); - _popup = e.NameScope.Find("PART_Popup"); + base.OnLoaded(e); + var root = this.ItemsPanelRoot; + if (root is OverflowStackPanel stack) + { + stack.OverflowPanel = _overflowPanel; + } } protected override void OnPointerPressed(PointerPressedEventArgs e) @@ -203,16 +235,9 @@ public class NavMenuItem: HeaderedSelectingItemsControl } else { - if (_popup is not null) + if (_border?.ContextFlyout is not null) { - if (_popup.IsOpen) - { - _popup.Close(); - } - else - { - _popup.Open(); - } + _border.ContextFlyout.ShowAt(this); } } } diff --git a/src/Ursa/Controls/NavMenu/OverflowStackPanel.cs b/src/Ursa/Controls/NavMenu/OverflowStackPanel.cs new file mode 100644 index 0000000..b55877a --- /dev/null +++ b/src/Ursa/Controls/NavMenu/OverflowStackPanel.cs @@ -0,0 +1,30 @@ +using Avalonia.Controls; + +namespace Ursa.Controls; + +public class OverflowStackPanel: StackPanel +{ + public Panel? OverflowPanel { get; set; } + public void MoveChildrenToOverflowPanel() + { + var children = this.Children.ToList(); + foreach (var child in children) + { + this.Children.Remove(child); + this.OverflowPanel?.Children.Add(child); + } + } + + public void MoveChildrenToMainPanel() + { + var children = this.OverflowPanel?.Children.ToList(); + if (children != null && children.Count > 0) + { + foreach (var child in children) + { + this.OverflowPanel?.Children.Remove(child); + this.Children.Add(child); + } + } + } +} \ No newline at end of file From ec41a8228ff429c651a9f7710ac0604b6dcc6fbe Mon Sep 17 00:00:00 2001 From: rabbitism Date: Wed, 14 Feb 2024 00:22:09 +0800 Subject: [PATCH 14/20] feat: use popup instead of flyout. improve demo. --- .../Converters/IconNameToPathConverter.cs | 36 ++++++ demo/Ursa.Demo/Pages/NavMenuDemo.axaml | 59 +++++----- .../ViewModels/NavMenuDemoViewModel.cs | 7 +- src/Ursa.Themes.Semi/Controls/NavMenu.axaml | 110 +++++++++++++----- .../Converters/NavMenuMarginConverter.cs | 4 +- src/Ursa/Controls/NavMenu/NavMenuItem.cs | 11 +- 6 files changed, 167 insertions(+), 60 deletions(-) create mode 100644 demo/Ursa.Demo/Converters/IconNameToPathConverter.cs diff --git a/demo/Ursa.Demo/Converters/IconNameToPathConverter.cs b/demo/Ursa.Demo/Converters/IconNameToPathConverter.cs new file mode 100644 index 0000000..5a602e8 --- /dev/null +++ b/demo/Ursa.Demo/Converters/IconNameToPathConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Globalization; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Ursa.Demo.Converters; + +public class IconNameToPathConverter: IValueConverter +{ + private string[] paths = new[] + { + "M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z", + "M16 12L9 2L2 12H3.86L0 18H7V22H11V18H18L14.14 12H16M20.14 12H22L15 2L12.61 5.41L17.92 13H15.97L19.19 18H24L20.14 12M13 19H17V22H13V19Z", + "M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z", + "M5 21C3.9 21 3 20.1 3 19V5C3 3.9 3.9 3 5 3H19C20.1 3 21 3.9 21 5V19C21 20.1 20.1 21 19 21H5M15.3 16L13.2 13.9L17 10L14.2 7.2L10.4 11.1L8.2 8.9V16H15.3Z", + "M16,9V7H12V12.5C11.58,12.19 11.07,12 10.5,12A2.5,2.5 0 0,0 8,14.5A2.5,2.5 0 0,0 10.5,17A2.5,2.5 0 0,0 13,14.5V9H16M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z", + "M16.67,4H15V2H9V4H7.33A1.33,1.33 0 0,0 6,5.33V20.67C6,21.4 6.6,22 7.33,22H16.67A1.33,1.33 0 0,0 18,20.67V5.33C18,4.6 17.4,4 16.67,4Z", + "M12 2C6.5 2 2 6.5 2 12C2 17.5 6.5 22 12 22C17.5 22 22 17.5 22 12S17.5 2 12 2M12.5 13H11V7H12.5V11.3L16.2 9.2L17 10.5L12.5 13Z" + }; + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is int i) + { + string s = paths[i % paths.Length]; + return StreamGeometry.Parse(s); + } + return AvaloniaProperty.UnsetValue; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml index 5a944cf..504c2eb 100644 --- a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml @@ -6,14 +6,17 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:u="https://irihi.tech/ursa" xmlns:vm="using:Ursa.Demo.ViewModels" + xmlns:converters="clr-namespace:Ursa.Demo.Converters" d:DesignHeight="450" d:DesignWidth="800" x:CompileBindings="True" x:DataType="vm:NavMenuDemoViewModel" mc:Ignorable="d"> + + + - + Collapse + + + + + + + + + + + - - - - - - - - - - - - + diff --git a/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs index 7748926..7da440b 100644 --- a/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/NavMenuDemoViewModel.cs @@ -1,4 +1,5 @@ -using System.Collections.ObjectModel; +using System; +using System.Collections.ObjectModel; using System.Threading.Tasks; using System.Windows.Input; using CommunityToolkit.Mvvm.ComponentModel; @@ -58,11 +59,13 @@ public class MenuItem { public string? Header { get; set; } public string? Icon { get; set; } + public int IconIndex { get; set; } public ICommand NavigationCommand { get; set; } - + static Random r = new Random(); public MenuItem() { NavigationCommand = new AsyncRelayCommand(OnNavigate); + IconIndex = r.Next(100); } private async Task OnNavigate() diff --git a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml index 0909f21..da22141 100644 --- a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml +++ b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml @@ -17,6 +17,10 @@ + @@ -24,20 +28,18 @@ - - - - - - - - + + @@ -46,12 +48,14 @@ + + + + + + RenderTransformOrigin="0.5,0"> - + + + - + - - - - + + + + + + + + + + + - @@ -244,30 +176,44 @@ - diff --git a/src/Ursa/Controls/NavMenu/NavMenuItem.cs b/src/Ursa/Controls/NavMenu/NavMenuItem.cs index 08fed11..f2550bf 100644 --- a/src/Ursa/Controls/NavMenu/NavMenuItem.cs +++ b/src/Ursa/Controls/NavMenu/NavMenuItem.cs @@ -124,7 +124,15 @@ public class NavMenuItem: HeaderedSelectingItemsControl 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() { @@ -222,6 +230,11 @@ public class NavMenuItem: HeaderedSelectingItemsControl protected override void OnPointerPressed(PointerPressedEventArgs e) { + if (IsSeparator) + { + e.Handled = true; + return; + } base.OnPointerPressed(e); if (this.ItemCount == 0) { From f67a5a313cc3de10e21897666ae4ff842a932c75 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Wed, 14 Feb 2024 01:25:21 +0800 Subject: [PATCH 17/20] feat: add toggle registration. --- demo/Ursa.Demo/Pages/NavMenuDemo.axaml | 87 ++++++++++++++------- src/Ursa.Themes.Semi/Controls/NavMenu.axaml | 3 + src/Ursa/Controls/NavMenu/NavMenu.cs | 30 +++++++ 3 files changed, 93 insertions(+), 27 deletions(-) diff --git a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml index ad61228..72a2f89 100644 --- a/demo/Ursa.Demo/Pages/NavMenuDemo.axaml +++ b/demo/Ursa.Demo/Pages/NavMenuDemo.axaml @@ -2,34 +2,50 @@ x:Class="Ursa.Demo.Pages.NavMenuDemo" 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" xmlns:vm="using:Ursa.Demo.ViewModels" - xmlns:converters="clr-namespace:Ursa.Demo.Converters" d:DesignHeight="450" d:DesignWidth="800" x:CompileBindings="True" x:DataType="vm:NavMenuDemoViewModel" mc:Ignorable="d"> - + - - Collapse - - + + + Collapse + + + - @@ -37,26 +53,43 @@ - + ActiveStrokeBrush="{DynamicResource SemiBlue5}" + Data="{Binding Converter={StaticResource IconConverter}}" + Foreground="{DynamicResource SemiGrey5}" + IsActive="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=u:NavMenuItem}, Path=IsHighlighted, Mode=TwoWay}" + StrokeBrush="{DynamicResource SemiGrey5}" /> - - - + + + + + + - - + + - - - + + + diff --git a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml index d174423..b8196a7 100644 --- a/src/Ursa.Themes.Semi/Controls/NavMenu.axaml +++ b/src/Ursa.Themes.Semi/Controls/NavMenu.axaml @@ -128,6 +128,9 @@