2
0
Эх сурвалжийг харах

Merge branch 'master' into line-node

Krzysztof Krysiński 3 долоо хоног өмнө
parent
commit
378643eee8

+ 135 - 1
src/PixiEditor/Helpers/Extensions/EnumerableExtensions.cs

@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System.Buffers;
+using System.Collections.Generic;
 
 namespace PixiEditor.Helpers.Extensions;
 
@@ -95,4 +96,137 @@ internal static class EnumerableExtensions
 
         return IndexOrNext(collection, predicate, index, false);
     }
+
+    public static T IndexOrNextInDirection<T>(this IEnumerable<T> collection, Predicate<T> predicate, int index, NextToDirection direction, bool overrun = true) => direction switch
+    {
+        NextToDirection.Forwards => IndexOrNext(collection, predicate, index, overrun),
+        NextToDirection.Backwards => IndexOrPrevious(collection, predicate, index, overrun),
+        _ => throw new ArgumentOutOfRangeException(nameof(direction)),
+    };
+    
+    /// <summary>
+    /// Returns the element that comes immediately after the specified <paramref name="index"/> 
+    /// in the given <paramref name="enumerable"/>, wrapping around to the first element if 
+    /// the end of the sequence is reached.
+    /// </summary>
+    /// <typeparam name="T">The type of the elements in the enumerable.</typeparam>
+    /// <param name="enumerable">The source enumerable.</param>
+    /// <param name="index">The index of the reference element. Must be non-negative.</param>
+    /// <returns>
+    /// The element immediately after the specified index, or the first element if the index 
+    /// refers to the last element. Returns <c>default</c> if the enumerable is empty.
+    /// </returns>
+    /// <remarks>
+    /// This method does not check whether the specified <paramref name="index"/> is within the 
+    /// bounds of the enumerable's size. Passing a positive out-of-range index may yield unexpected results.
+    /// </remarks>
+    /// <exception cref="ArgumentNullException">Thrown if <paramref name="enumerable"/> is <c>null</c>.</exception>
+    /// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="index"/> is negative.</exception>
+    public static T? WrapNextAfterIndex<T>(this IEnumerable<T> enumerable, int index)
+    {
+        ArgumentOutOfRangeException.ThrowIfNegative(index);
+        ArgumentNullException.ThrowIfNull(enumerable);
+
+        switch (enumerable)
+        {
+            case ICollection<T> collection:
+                return NextWithKnownCount(collection, index, collection.Count);
+            case IReadOnlyCollection<T> readOnlyCollection:
+                return NextWithKnownCount(readOnlyCollection, index, readOnlyCollection.Count);
+        }
+
+        using var enumerator = enumerable.GetEnumerator();
+
+        // If the enumerable is empty, return null
+        if (!enumerator.MoveNext())
+            return default;
+
+        var steps = index + 1;
+        var firstElement = enumerator.Current;
+
+        while (steps-- > 0)
+        {
+            if (!enumerator.MoveNext())
+                return firstElement;
+        }
+        
+        return enumerator.Current;
+    }
+
+    /// <summary>
+    /// Returns the element that comes immediately before the specified <paramref name="index"/> 
+    /// in the given <paramref name="enumerable"/>, wrapping around to the last element if 
+    /// the start of the sequence is reached.
+    /// </summary>
+    /// <typeparam name="T">The type of the elements in the enumerable.</typeparam>
+    /// <param name="enumerable">The source enumerable.</param>
+    /// <param name="index">The index of the reference element. Must be non-negative.</param>
+    /// <returns>
+    /// The element immediately before the specified index, or the last element if the index 
+    /// is <c>0</c>. Returns <c>default</c> if the enumerable is empty.
+    /// </returns>
+    /// <remarks>
+    /// This method does not check whether the specified <paramref name="index"/> is within the 
+    /// bounds of the enumerable's size. Passing a positive out-of-range index may yield unexpected results.
+    /// </remarks>
+    /// <exception cref="ArgumentNullException">Thrown if <paramref name="enumerable"/> is <c>null</c>.</exception>
+    /// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="index"/> is negative.</exception>
+    public static T? WrapPreviousBeforeIndex<T>(this IEnumerable<T> enumerable, int index)
+    {
+        ArgumentOutOfRangeException.ThrowIfNegative(index);
+        ArgumentNullException.ThrowIfNull(enumerable);
+        
+        return index == 0
+            ? enumerable.LastOrDefault()
+            : enumerable.ElementAtOrDefault(index - 1);
+    }
+
+    /// <summary>
+    /// Returns the element next to the specified <paramref name="index"/> in the given 
+    /// <paramref name="enumerable"/>, in the direction specified by <paramref name="direction"/>, 
+    /// wrapping around if necessary.
+    /// </summary>
+    /// <typeparam name="T">The type of the elements in the enumerable.</typeparam>
+    /// <param name="enumerable">The source enumerable.</param>
+    /// <param name="index">The index of the reference element. Must be non-negative.</param>
+    /// <param name="direction">
+    /// The direction in which to look for the next element (forwards or backwards).
+    /// </param>
+    /// <returns>
+    /// The element next to the specified index in the chosen direction, with wrap-around behavior.
+    /// Returns <c>default</c> if the enumerable is empty.
+    /// </returns>
+    /// <remarks>
+    /// This method does not check whether the specified <paramref name="index"/> is within the 
+    /// bounds of the enumerable's size. Passing a positive out-of-range index may yield unexpected results.
+    /// </remarks>
+    /// <exception cref="ArgumentNullException">Thrown if <paramref name="enumerable"/> is <c>null</c>.</exception>
+    /// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="index"/> is negative.</exception>
+    /// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="direction"/> is not a valid <see cref="NextToDirection"/> value.</exception>
+    public static T? WrapInDirectionOfIndex<T>(this IEnumerable<T> enumerable, int index, NextToDirection direction) =>
+        direction switch
+        {
+            NextToDirection.Forwards => WrapNextAfterIndex(enumerable, index),
+            NextToDirection.Backwards => WrapPreviousBeforeIndex(enumerable, index),
+            _ => throw new ArgumentOutOfRangeException(nameof(direction)),
+        };
+
+    private static T? NextWithKnownCount<T>(IEnumerable<T> collection, int index, int count)
+    {
+        if (count == 0)
+            return default;
+        
+        var newIndex = index + 1;
+
+        if (newIndex < 0 || newIndex >= count)
+            newIndex = newIndex < 0 ? count - 1 : 0;
+        
+        return collection.ElementAtOrDefault(newIndex);
+    }
+}
+
+enum NextToDirection
+{
+    Forwards = 1,
+    Backwards = -1
 }

+ 15 - 1
src/PixiEditor/Styles/Templates/NodePicker.axaml

@@ -4,7 +4,8 @@
                     xmlns:visuals="clr-namespace:PixiEditor.Views.Visuals"
                     xmlns:ui="clr-namespace:PixiEditor.UI.Common.Localization;assembly=PixiEditor.UI.Common"
                     xmlns:input="clr-namespace:PixiEditor.Views.Input"
-                    xmlns:nodes1="clr-namespace:PixiEditor.ViewModels.Nodes">
+                    xmlns:nodes1="clr-namespace:PixiEditor.ViewModels.Nodes"
+                    xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters">
     <ControlTheme TargetType="nodes:NodePicker" x:Key="{x:Type nodes:NodePicker}">
         <Setter Property="Template">
             <ControlTemplate>
@@ -55,6 +56,19 @@
                                                         CommandParameter="{Binding}"
                                                         HorizontalContentAlignment="Left"
                                                         IsVisible="{Binding !Hidden}">
+                                                        <Classes.KeyboardSelected>
+                                                            <MultiBinding Converter="{converters:AreEqualConverter}">
+                                                                <Binding />
+                                                                <Binding Path="SelectedNode" RelativeSource="{RelativeSource FindAncestor, AncestorType=nodes:NodePicker}" Mode="OneWay"/>
+                                                            </MultiBinding>
+                                                        </Classes.KeyboardSelected>
+                                                        
+                                                        <Button.Styles>
+                                                            <Style Selector="Button.KeyboardSelected">
+                                                                <Setter Property="Background" Value="{DynamicResource ThemeControlHighlightBrush}" />
+                                                            </Style>
+                                                        </Button.Styles>
+                                                        
                                                         <TextBlock Margin="10 0 0 0">
                                                             <Run Classes="pixi-icon"
                                                                  BaselineAlignment="Center"

+ 5 - 9
src/PixiEditor/Views/Main/CommandSearch/CommandSearchControl.axaml.cs

@@ -184,11 +184,11 @@ internal partial class CommandSearchControl : UserControl, INotifyPropertyChange
         }
         else if (e.Key is Key.Down or Key.PageDown)
         {
-            MoveSelection(1);
+            MoveSelection(NextToDirection.Forwards);
         }
         else if (e.Key is Key.Up or Key.PageUp)
         {
-            MoveSelection(-1);
+            MoveSelection(NextToDirection.Backwards);
         }
         else if (e.Key == Key.Escape ||
                  CommandController.Current.Commands["PixiEditor.Search.Toggle"].Shortcut
@@ -266,22 +266,18 @@ internal partial class CommandSearchControl : UserControl, INotifyPropertyChange
         }
     }
 
-    private void MoveSelection(int delta)
+    private void MoveSelection(NextToDirection direction)
     {
-        if (delta == 0)
-            return;
         if (SelectedResult is null)
         {
             SelectedResult = Results.FirstOrDefault(x => x.CanExecute);
             return;
         }
 
-        int newIndex = Results.IndexOf(SelectedResult) + delta;
+        var newIndex = Results.IndexOf(SelectedResult) + (int)direction;
         newIndex = (newIndex % Results.Count + Results.Count) % Results.Count;
 
-        SelectedResult = delta > 0
-            ? Results.IndexOrNext(x => x.CanExecute, newIndex)
-            : Results.IndexOrPrevious(x => x.CanExecute, newIndex);
+        SelectedResult = Results.IndexOrNextInDirection(x => x.CanExecute, newIndex, direction);
 
         newIndex = Results.IndexOf(SelectedResult);
         itemscontrol.ContainerFromIndex(newIndex)?.BringIntoView();

+ 82 - 8
src/PixiEditor/Views/Nodes/NodePicker.cs

@@ -5,7 +5,10 @@ using Avalonia.Controls;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Threading;
+using Avalonia.VisualTree;
+using PixiEditor.Helpers.Extensions;
 using PixiEditor.Helpers.Nodes;
 using PixiEditor.ViewModels.Nodes;
 using PixiEditor.Views.Input;
@@ -43,6 +46,15 @@ public partial class NodePicker : TemplatedControl
         set => SetValue(SelectedCategoryProperty, value);
     }
 
+    public static readonly StyledProperty<NodeTypeInfo> SelectedNodeProperty =
+        AvaloniaProperty.Register<NodePicker, NodeTypeInfo>(nameof(SelectedNode));
+
+    public NodeTypeInfo? SelectedNode
+    {
+        get => GetValue(SelectedNodeProperty);
+        set => SetValue(SelectedNodeProperty, value);
+    }
+
     public ObservableCollection<NodeTypeInfo> AllNodeTypeInfos
     {
         get => GetValue(AllNodeTypeInfosProperty);
@@ -113,6 +125,7 @@ public partial class NodePicker : TemplatedControl
     {
         _inputBox = e.NameScope.Find<InputBox>("PART_InputBox");
 
+        _inputBox.Loaded += (_, _) => _inputBox.SelectAll();
         _inputBox.KeyDown += OnInputBoxKeyDown;
 
         _itemsControl = e.NameScope.Find<ItemsControl>("PART_NodeList");
@@ -158,6 +171,8 @@ public partial class NodePicker : TemplatedControl
             return;
         }
 
+        nodePicker.SelectedNode = null;
+        
         if (NodeAbbreviation.IsAbbreviation(nodePicker.SearchQuery, out var abbreviationName))
         {
             nodePicker.FilteredNodeGroups = nodePicker.NodeTypeGroupsFromQuery(abbreviationName);
@@ -235,8 +250,22 @@ public partial class NodePicker : TemplatedControl
 
     private void OnInputBoxKeyDown(object? sender, KeyEventArgs e)
     {
-        if (e.Key != Key.Enter)
+        switch (e.Key)
         {
+            case Key.Enter:
+                HandleEnterDown(sender, e);
+                return;
+            case Key.Down or Key.Up:
+                HandleKeyUpDown(sender, e); 
+                return;
+        }
+    }
+
+    private void HandleEnterDown(object? sender, KeyEventArgs e)
+    {
+        if (SelectedNode != null)
+        {
+            SelectNodeCommand.Execute(SelectedNode);
             return;
         }
 
@@ -247,17 +276,62 @@ public partial class NodePicker : TemplatedControl
             return;
         }
 
-        if (nodes == null && FilteredNodeGroups.Count > 0)
+        foreach (var node in nodes)
         {
-            SelectNodeCommand.Execute(FilteredNodeGroups[0]);
+            SelectNodeCommand.Execute(node);
         }
-        else
+    }
+
+    private void HandleKeyUpDown(object? sender, KeyEventArgs e)
+    {
+        if (SelectedNode == null)
         {
-            foreach (var node in nodes)
-            {
-                SelectNodeCommand.Execute(node);
-            }
+            SelectedNode = e.Key == Key.Down
+                ? FilteredNodeGroups.FirstOrDefault()?.NodeTypes.FirstOrDefault()
+                : FilteredNodeGroups.LastOrDefault()?.NodeTypes.LastOrDefault();
+                
+            return;
+        }
+
+        var direction = e.Key == Key.Down ? NextToDirection.Forwards : NextToDirection.Backwards;
+        SelectedNode = GetNodeNextTo(FilteredNodeGroups, SelectedNode, direction, out var group);
+
+        var container = _itemsControl.ContainerFromItem(group);
+        var buttonList = container.FindDescendantOfType<ItemsControl>();
+        
+        var button = buttonList.ContainerFromItem(SelectedNode);
+
+        const double padding = 2.6;
+        const double paddingHeight = padding * 2 + 1;
+        
+        // Bring Button above/below also into view
+        button.BringIntoView(new Rect(0, button.Bounds.Height * -padding, button.Bounds.Width, button.Bounds.Height * paddingHeight));
+    }
+
+    private static NodeTypeInfo? GetNodeNextTo(ObservableCollection<NodeTypeGroup> groups, NodeTypeInfo node, NextToDirection direction, out NodeTypeGroup group)
+    {
+        var currentGroup = groups.FirstOrDefault(x => x.NodeTypes.Contains(node));
+
+        group = currentGroup;
+        if (currentGroup == null)
+            return null;
+        
+        var indexInGroup = currentGroup.NodeTypes.IndexOf(node);
+        var groupIndex = groups.IndexOf(currentGroup);
+
+        if (direction == NextToDirection.Backwards && indexInGroup == 0)
+        {
+            group = groups.WrapPreviousBeforeIndex(groupIndex);
+            return group.NodeTypes.Last();
         }
+
+        if (direction == NextToDirection.Forwards && indexInGroup == currentGroup.NodeTypes.Count - 1)
+        {
+            group = groups.WrapNextAfterIndex(groupIndex);
+            return group.NodeTypes.First();
+        }
+
+        return currentGroup.NodeTypes[indexInGroup + (int)direction];
     }
 
     private static bool SearchComparer(NodeTypeInfo x, string lookFor) =>