diff --git a/src/Ursa/Controls/NavMenu/NavMenu.cs b/src/Ursa/Controls/NavMenu/NavMenu.cs index ec00428..bbe036d 100644 --- a/src/Ursa/Controls/NavMenu/NavMenu.cs +++ b/src/Ursa/Controls/NavMenu/NavMenu.cs @@ -189,6 +189,16 @@ public class NavMenu : ItemsControl, ICustomKeyboardNavigation add => AddHandler(SelectionChangedEvent, value); remove => RemoveHandler(SelectionChangedEvent, value); } + + + public static readonly RoutedEvent SelectionChangingEvent = + RoutedEvent.Register(nameof(SelectionChanging), RoutingStrategies.Bubble); + + public event EventHandler SelectionChanging + { + add => AddHandler(SelectionChangingEvent, value); + remove => RemoveHandler(SelectionChangingEvent, value); + } protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { @@ -486,4 +496,28 @@ public class NavMenu : ItemsControl, ICustomKeyboardNavigation return null; } + + internal bool CanChangeSelection(NavMenuItem item) + { + object? newSelection = null; + if (item.DataContext is not null && item.DataContext != DataContext) + newSelection = item.DataContext; + else + newSelection = item; + var args = new SelectionChangingEventArgs( + SelectionChangingEvent, + new[] { SelectedItem }, + new[] { newSelection }) + { + Source = this, + }; + RaiseEvent(args); + var result = args.CanSelect; + if (result == false) + { + var container = GetContainerForItem(SelectedItem); + container?.Focus(NavigationMethod.Directional); + } + return result; + } } \ No newline at end of file diff --git a/src/Ursa/Controls/NavMenu/NavMenuItem.cs b/src/Ursa/Controls/NavMenu/NavMenuItem.cs index 1ba56fb..378cf6c 100644 --- a/src/Ursa/Controls/NavMenu/NavMenuItem.cs +++ b/src/Ursa/Controls/NavMenu/NavMenuItem.cs @@ -381,6 +381,10 @@ public class NavMenuItem : HeaderedItemsControl internal void SelectItem(NavMenuItem item) { + if (item == this && RootMenu?.CanChangeSelection(item) != true) + { + return; + } SetCurrentValue(IsSelectedProperty, item == this); SetCurrentValue(IsHighlightedProperty, true); diff --git a/src/Ursa/Controls/NavMenu/SelectionChangingEventArgs.cs b/src/Ursa/Controls/NavMenu/SelectionChangingEventArgs.cs new file mode 100644 index 0000000..0e8e448 --- /dev/null +++ b/src/Ursa/Controls/NavMenu/SelectionChangingEventArgs.cs @@ -0,0 +1,24 @@ +using System.Collections; +using Avalonia.Interactivity; + +namespace Ursa.Controls; + +public class SelectionChangingEventArgs: RoutedEventArgs +{ + /// Gets the items that were added to the selection. + public IList NewItems { get; } + + /// Gets the items that were removed from the selection. + public IList OldItems { get; } + + /// + /// Gets or sets a value indicating whether the selection can be changed. If set to false, the selection will not change. + /// + public bool CanSelect { get; set; } = true; + + public SelectionChangingEventArgs(RoutedEvent routedEvent, IList oldItems, IList newItems): base(routedEvent) + { + OldItems = oldItems; + NewItems = newItems; + } +} \ No newline at end of file diff --git a/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/Test.cs b/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/Test.cs new file mode 100644 index 0000000..f4a956d --- /dev/null +++ b/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/Test.cs @@ -0,0 +1,103 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless; +using Avalonia.Headless.XUnit; +using Avalonia.LogicalTree; +using Ursa.Controls; + +namespace HeadlessTest.Ursa.Controls.NavMenuTests.CanSelectTests; + +public class Test +{ + [AvaloniaFact] + public void CanSelect_Blocks_Selection_Change_Inline_Xaml() + { + Window window = new Window + { + Width = 400, + Height = 400, + }; + + var view = new TestView1(); + + window.Content = view; + + window.Show(); + + var menu = view.FindControl("Menu"); + var item1 = view.FindControl("MenuItem1"); + var item2 = view.FindControl("MenuItem2"); + var item3 = view.FindControl("MenuItem3"); + + Assert.NotNull(menu); + Assert.NotNull(item1); + Assert.NotNull(item2); + Assert.NotNull(item3); + + var point1 = item1.TranslatePoint(new Point(0, 0), window); + var point2 = item2.TranslatePoint(new Point(0, 0), window); + var point3 = item3.TranslatePoint(new Point(0, 0), window); + Assert.NotNull(point1); + Assert.NotNull(point2); + Assert.NotNull(point3); + + window.MouseDown(new Point(point1.Value.X+10, point1.Value.Y+10), Avalonia.Input.MouseButton.Left); + window.MouseUp(new Point(point1.Value.X+10, point1.Value.Y+10), Avalonia.Input.MouseButton.Left); + Assert.Equal(item1, menu.SelectedItem); + + window.MouseDown(new Point(point2.Value.X+10, point2.Value.Y+10), Avalonia.Input.MouseButton.Left); + window.MouseUp(new Point(point2.Value.X+10, point2.Value.Y+10), Avalonia.Input.MouseButton.Left); + Assert.Equal(item1, menu.SelectedItem); // Should not change selection due to CanSelect being false + + window.MouseDown(new Point(point3.Value.X+10, point3.Value.Y+10), Avalonia.Input.MouseButton.Left); + window.MouseUp(new Point(point3.Value.X+10, point3.Value.Y+10), Avalonia.Input.MouseButton.Left); + Assert.Equal(item3, menu.SelectedItem); // Should change selection to item3 + + } + + [AvaloniaFact] + public void CanSelect_Blocks_Selection_Change_Inline_Code() + { + Window window = new Window + { + Width = 400, + Height = 400, + }; + + var view = new TestView2(); + + window.Content = view; + + window.Show(); + + var menu = view.FindControl("Menu"); + var items = menu.GetLogicalDescendants().OfType().ToList(); + var item1 = items[0]; + var item2 = items[1]; + var item3 = items[2]; + + Assert.NotNull(menu); + Assert.NotNull(item1); + Assert.NotNull(item2); + Assert.NotNull(item3); + + var point1 = item1.TranslatePoint(new Point(0, 0), window); + var point2 = item2.TranslatePoint(new Point(0, 0), window); + var point3 = item3.TranslatePoint(new Point(0, 0), window); + Assert.NotNull(point1); + Assert.NotNull(point2); + Assert.NotNull(point3); + + window.MouseDown(new Point(point1.Value.X+10, point1.Value.Y+10), Avalonia.Input.MouseButton.Left); + window.MouseUp(new Point(point1.Value.X+10, point1.Value.Y+10), Avalonia.Input.MouseButton.Left); + Assert.Equal(item1.DataContext, menu.SelectedItem); + + window.MouseDown(new Point(point2.Value.X+10, point2.Value.Y+10), Avalonia.Input.MouseButton.Left); + window.MouseUp(new Point(point2.Value.X+10, point2.Value.Y+10), Avalonia.Input.MouseButton.Left); + Assert.Equal(item1.DataContext, menu.SelectedItem); // Should not change selection due to CanSelect being false + + window.MouseDown(new Point(point3.Value.X+10, point3.Value.Y+10), Avalonia.Input.MouseButton.Left); + window.MouseUp(new Point(point3.Value.X+10, point3.Value.Y+10), Avalonia.Input.MouseButton.Left); + Assert.Equal(item3.DataContext, menu.SelectedItem); // Should change selection to item3 + } +} \ No newline at end of file diff --git a/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/TestView1.axaml b/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/TestView1.axaml new file mode 100644 index 0000000..187ddc0 --- /dev/null +++ b/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/TestView1.axaml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/TestView1.axaml.cs b/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/TestView1.axaml.cs new file mode 100644 index 0000000..25b61b6 --- /dev/null +++ b/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/TestView1.axaml.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Ursa.Controls; + +namespace HeadlessTest.Ursa.Controls.NavMenuTests.CanSelectTests; + +public partial class TestView1 : UserControl +{ + public TestView1() + { + InitializeComponent(); + } + + private void Menu_OnSelectionChanging(object? sender, SelectionChangingEventArgs e) + { + var newItem = e.NewItems; + if (newItem is [NavMenuItem { Name: "MenuItem2" }]) + { + e.CanSelect = false; // Prevent selection change for MenuItem2 + } + } +} \ No newline at end of file diff --git a/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/TestView2.axaml b/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/TestView2.axaml new file mode 100644 index 0000000..d38a0de --- /dev/null +++ b/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/TestView2.axaml @@ -0,0 +1,12 @@ + + + diff --git a/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/TestView2.axaml.cs b/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/TestView2.axaml.cs new file mode 100644 index 0000000..54d5bbb --- /dev/null +++ b/tests/HeadlessTest.Ursa/Controls/NavMenuTests/CanSelectTests/TestView2.axaml.cs @@ -0,0 +1,43 @@ +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using CommunityToolkit.Mvvm.ComponentModel; +using Ursa.Controls; + +namespace HeadlessTest.Ursa.Controls.NavMenuTests.CanSelectTests; + +public partial class TestView2 : UserControl +{ + public TestView2() + { + InitializeComponent(); + this.DataContext = new TestView2ViewModel(); + } + + private void NavMenu_OnSelectionChanging(object? sender, SelectionChangingEventArgs e) + { + if (e.NewItems is [MenuItemViewModel item]) + { + if (item.Text.Contains("2")) + { + e.CanSelect = false; + } + } + } +} + +public partial class TestView2ViewModel +{ + public ObservableCollection MenuItems { get; } = new() + { + new MenuItemViewModel { Text = "Menu Item 1" }, + new MenuItemViewModel { Text = "Menu Item 2" }, + new MenuItemViewModel { Text = "Menu Item 3" } + }; +} + +public partial class MenuItemViewModel: ObservableObject +{ + [ObservableProperty] private string? _text; +} \ No newline at end of file