瀏覽代碼

Added ToggleGroup for enum selection

Krzysztof Krysiński 1 月之前
父節點
當前提交
3123afdd3e

+ 3 - 3
src/PixiEditor/Data/Localization/Languages/en.json

@@ -1223,7 +1223,7 @@
   "FLIP_Y": "Flip Y",
   "STABILIZATION_SETTING": "Stabilization",
   "STABILIZATION_MODE_SETTING": "Stabilization Mode",
-  "NONE": "None",
-  "DISTANCE_BASED": "Distance Based",
-  "TIME_BASED": "Time Based"
+  "NONE_STABILIZATION": "None - No stabilization is applied",
+  "DISTANCE_BASED_STABILIZATION": "Distance Based - Stabilization based on pointer movement distance. Movement is applied when pointer moves more than the specified distance.",
+  "TIME_BASED_STABILIZATION": "Time Based - Stabilization based on time. Amount of smoothing is dependent on how much time has passed between points."
 }

+ 126 - 0
src/PixiEditor/Helpers/Behaviours/ToggleGroupBehavior.cs

@@ -0,0 +1,126 @@
+using Avalonia;
+using Avalonia.Controls.Primitives;
+
+namespace PixiEditor.Helpers.Behaviours;
+
+public static class ToggleGroupBehavior
+{
+    // The group identifier
+    public static readonly AttachedProperty<string?> GroupNameProperty =
+        AvaloniaProperty.RegisterAttached<ToggleButton, string?>("GroupName", typeof(ToggleGroupBehavior));
+
+    // The enum (or any value) this button represents
+    public static readonly AttachedProperty<object?> ValueProperty =
+        AvaloniaProperty.RegisterAttached<ToggleButton, object?>("Value", typeof(ToggleGroupBehavior));
+
+    // The current selected value for the entire group (bind this!)
+    public static readonly AttachedProperty<object?> SelectedValueProperty =
+        AvaloniaProperty.RegisterAttached<AvaloniaObject, object?>("SelectedValue", typeof(ToggleGroupBehavior));
+
+    private static readonly Dictionary<string, List<ToggleButton>> _groups = new();
+
+    static ToggleGroupBehavior()
+    {
+        GroupNameProperty.Changed.AddClassHandler<ToggleButton>(OnGroupNameChanged);
+        ValueProperty.Changed.AddClassHandler<ToggleButton>(OnValueChanged);
+        SelectedValueProperty.Changed.AddClassHandler<AvaloniaObject>(OnSelectedValueChanged);
+    }
+
+    public static void SetGroupName(AvaloniaObject element, string? value) =>
+        element.SetValue(GroupNameProperty, value);
+
+    public static string? GetGroupName(AvaloniaObject element) =>
+        element.GetValue(GroupNameProperty);
+
+    public static void SetValue(AvaloniaObject element, object? value) =>
+        element.SetValue(ValueProperty, value);
+
+    public static object? GetValue(AvaloniaObject element) =>
+        element.GetValue(ValueProperty);
+
+    public static void SetSelectedValue(AvaloniaObject element, object? value) =>
+        element.SetValue(SelectedValueProperty, value);
+
+    public static object? GetSelectedValue(AvaloniaObject element) =>
+        element.GetValue(SelectedValueProperty);
+
+    private static void OnGroupNameChanged(ToggleButton button, AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.OldValue is string oldGroup && _groups.TryGetValue(oldGroup, out var oldList))
+            oldList.Remove(button);
+
+        if (e.NewValue is string newGroup)
+        {
+            if (!_groups.TryGetValue(newGroup, out var list))
+            {
+                list = new List<ToggleButton>();
+                _groups[newGroup] = list;
+            }
+
+            list.Add(button);
+            button.Checked += ButtonChecked;
+            button.Click += ButtonClickPreventUntoggle;
+        }
+    }
+
+    private static void OnValueChanged(ToggleButton button, AvaloniaPropertyChangedEventArgs e)
+    {
+        UpdateIsChecked(button);
+    }
+
+    private static void OnSelectedValueChanged(AvaloniaObject o, AvaloniaPropertyChangedEventArgs e)
+    {
+        // Find all buttons with matching group and update their IsChecked
+        foreach (var group in _groups)
+        {
+            foreach (var btn in group.Value)
+            {
+                UpdateIsChecked(btn);
+            }
+        }
+    }
+
+    private static void ButtonChecked(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+    {
+        if (sender is ToggleButton btn)
+        {
+            var group = GetGroupName(btn);
+            var value = GetValue(btn);
+
+            // Set group's SelectedValue to this button's value
+            if (group is not null)
+            {
+                foreach (var kv in _groups)
+                {
+                    foreach (var b in kv.Value)
+                    {
+                        if (b != btn && GetGroupName(b) == group)
+                            b.IsChecked = false;
+                    }
+                }
+            }
+
+            // Update bound SelectedValue on parent DataContext
+            SetSelectedValue(btn, value);
+        }
+    }
+
+    private static void ButtonClickPreventUntoggle(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+    {
+        if (sender is ToggleButton btn &&
+            btn.IsChecked == false &&
+            Equals(GetSelectedValue(btn), GetValue(btn)))
+        {
+            // Prevent unchecking current selection
+            btn.IsChecked = true;
+            e.Handled = true;
+        }
+    }
+
+    private static void UpdateIsChecked(ToggleButton btn)
+    {
+        var selected = GetSelectedValue(btn);
+        var value = GetValue(btn);
+        btn.IsChecked = Equals(selected, value);
+    }
+}

+ 31 - 0
src/PixiEditor/Helpers/Converters/EnumToIconConverter.cs

@@ -0,0 +1,31 @@
+using System.Globalization;
+using PixiEditor.Helpers.Decorators;
+using PixiEditor.UI.Common.Fonts;
+
+namespace PixiEditor.Helpers.Converters;
+
+internal class EnumToIconConverter : SingleInstanceConverter<EnumToIconConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value == null)
+            return null;
+
+        var enumType = value.GetType();
+        var enumName = Enum.GetName(enumType, value);
+        if (enumName == null)
+            return null;
+
+        var enumMember = enumType.GetMember(enumName).FirstOrDefault();
+        if (enumMember == null)
+            return null;
+
+        var iconAttribute = enumMember.GetCustomAttributes(typeof(IconNameAttribute), false)
+            .FirstOrDefault() as IconNameAttribute;
+        if (iconAttribute == null)
+            return null;
+
+        var icon = PixiPerfectIconExtensions.TryGetByName(iconAttribute.IconName);
+        return icon;
+    }
+}

+ 12 - 0
src/PixiEditor/Helpers/Decorators/IconNameAttribute.cs

@@ -0,0 +1,12 @@
+namespace PixiEditor.Helpers.Decorators;
+
+[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
+public class IconNameAttribute : Attribute
+{
+    public string IconName { get; }
+
+    public IconNameAttribute(string iconName)
+    {
+        IconName = iconName;
+    }
+}

+ 7 - 3
src/PixiEditor/Models/Handlers/Toolbars/IBrushToolbar.cs

@@ -1,6 +1,7 @@
 using System.ComponentModel;
 using PixiEditor.ChangeableDocument.Changeables.Brushes;
 using PixiEditor.Helpers;
+using PixiEditor.Helpers.Decorators;
 using PixiEditor.Models.BrushEngine;
 using PixiEditor.Views.Overlays.BrushShapeOverlay;
 
@@ -18,10 +19,13 @@ internal interface IBrushToolbar : IToolbar, IToolSizeToolbar
 
 public enum StabilizationMode
 {
-    [Description("NONE")]
+    [Description("NONE_STABILIZATION")]
+    [IconName("icon-sun")]
     None,
-    [Description("TIME_BASED")]
+    [Description("TIME_BASED_STABILIZATION")]
+    [IconName("icon-clock")]
     TimeBased,
-    [Description("DISTANCE_BASED")]
+    [Description("DISTANCE_BASED_STABILIZATION")]
+    [IconName("icon-circle")]
     CircleRope
 }

+ 27 - 2
src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/EnumSettingViewModel.cs

@@ -1,15 +1,19 @@
-using Avalonia;
+using System.Windows.Input;
+using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Data;
 using Avalonia.Layout;
+using CommunityToolkit.Mvvm.Input;
 using PixiEditor.Extensions.Helpers;
 using PixiEditor.Extensions.UI;
+using PixiEditor.Helpers.Decorators;
 
 namespace PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 
-internal sealed class EnumSettingViewModel<TEnum> : Setting<TEnum, ComboBox>
+internal sealed class EnumSettingViewModel<TEnum> : Setting<TEnum>
     where TEnum : struct, Enum
 {
+    private EnumSettingPickerType pickerType = EnumSettingPickerType.ComboBox;
     private int selectedIndex;
 
     /// <summary>
@@ -49,13 +53,28 @@ internal sealed class EnumSettingViewModel<TEnum> : Setting<TEnum, ComboBox>
             base.Value = value;
         }
     }
+
+    public EnumSettingPickerType PickerType
+    {
+        get => pickerType;
+        set
+        {
+            SetProperty(ref pickerType, value);
+            OnPropertyChanged(nameof(PickerIsIconButtons));
+        }
+    }
+
+    public bool PickerIsIconButtons => PickerType == EnumSettingPickerType.IconButtons;
     
     public TEnum[] EnumValues { get; } = Enum.GetValues<TEnum>();
 
+    public ICommand ChangeValueCommand { get; }
+
     public EnumSettingViewModel(string name, string label)
         : base(name)
     {
         Label = label;
+        ChangeValueCommand = new RelayCommand<TEnum>(val => Value = val);
     }
 
     public EnumSettingViewModel(string name, string label, TEnum defaultValue)
@@ -87,3 +106,9 @@ internal sealed class EnumSettingViewModel<TEnum> : Setting<TEnum, ComboBox>
         return Enum.GetValues<TEnum>()[index];
     }
 }
+
+public enum EnumSettingPickerType
+{
+    ComboBox,
+    IconButtons
+}

+ 0 - 9
src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/Setting.cs

@@ -7,15 +7,6 @@ using PixiEditor.UI.Common.Localization;
 
 namespace PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 
-internal abstract class Setting<T, TControl> : Setting<T>
-    where TControl : Control
-{
-    protected Setting(string name)
-        : base(name)
-    {
-    }
-}
-
 internal abstract class Setting<T> : Setting
 {
     protected Setting(string name)

+ 1 - 1
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/BrushToolbar.cs

@@ -73,7 +73,7 @@ internal class BrushToolbar : Toolbar, IBrushToolbar
         setting.ValueChanged += (_, _) => OnPropertyChanged(nameof(ToolSize));
         AddSetting(setting);
         AddSetting(new BrushSettingViewModel(nameof(Brush), "BRUSH_SETTING") { IsExposed = true });
-        AddSetting(new EnumSettingViewModel<StabilizationMode>(nameof(StabilizationMode), "STABILIZATION_MODE_SETTING") { IsExposed = true });
+        AddSetting(new EnumSettingViewModel<StabilizationMode>(nameof(StabilizationMode), "STABILIZATION_MODE_SETTING") { IsExposed = true, PickerType = EnumSettingPickerType.IconButtons, IsLabelVisible = false});
         AddSetting(new SizeSettingViewModel(nameof(Stabilization), "STABILIZATION_SETTING", 0, min: 0, max: 128) { IsExposed = true });
 
         foreach (var aSetting in Settings)

+ 0 - 2
src/PixiEditor/Views/Overlays/BrushShapeOverlay/BrushShapeOverlay.cs

@@ -13,11 +13,9 @@ using PixiEditor.ChangeableDocument.Rendering.ContextData;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Handlers.Toolbars;
-using PixiEditor.UI.Common.Extensions;
 using PixiEditor.Views.Rendering;
 using Canvas = Drawie.Backend.Core.Surfaces.Canvas;
 using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
-using IBrush = Avalonia.Media.IBrush;
 
 namespace PixiEditor.Views.Overlays.BrushShapeOverlay;
 #nullable enable

+ 68 - 19
src/PixiEditor/Views/Tools/ToolSettings/Settings/EnumSettingView.axaml

@@ -6,26 +6,75 @@
              xmlns:enums="clr-namespace:PixiEditor.ChangeableDocument.Enums;assembly=PixiEditor.ChangeableDocument"
              xmlns:localization="clr-namespace:PixiEditor.UI.Common.Localization;assembly=PixiEditor.UI.Common"
              xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+             xmlns:settings1="clr-namespace:PixiEditor.Views.Tools.ToolSettings.Settings"
+             xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
-             x:Class="PixiEditor.Views.Tools.ToolSettings.Settings.EnumSettingView">
+             x:Class="PixiEditor.Views.Tools.ToolSettings.Settings.EnumSettingView" Name="uc">
     <Design.DataContext>
         <settings:EnumSettingViewModel x:TypeArguments="enums:BlendMode" />
     </Design.DataContext>
-    
-    <ComboBox VerticalAlignment="Center"
-              MinWidth="85"
-              SelectedIndex="{Binding Value, Mode=TwoWay}" 
-              ItemsSource="{Binding EnumValues}">
-        <ComboBox.ItemContainerTheme>
-            <ControlTheme TargetType="ComboBoxItem" BasedOn="{StaticResource {x:Type ComboBoxItem}}">
-                <Setter Property="Tag" Value="{Binding .}"/>
-                <Setter Property="(localization:Translator.Key)" Value="{Binding ., Converter={converters:EnumToLocalizedStringConverter}}"/>
-            </ControlTheme>
-        </ComboBox.ItemContainerTheme>
-       <ComboBox.ItemTemplate>
-           <DataTemplate>
-               <TextBlock localization:Translator.Key="{Binding ., Converter={converters:EnumToLocalizedStringConverter}}"/>
-           </DataTemplate>
-       </ComboBox.ItemTemplate>
-    </ComboBox>
-</UserControl>
+
+    <Panel>
+        <ComboBox VerticalAlignment="Center"
+                  MinWidth="85"
+                  IsVisible="{Binding !PickerIsIconButtons}"
+                  SelectedIndex="{Binding Value, Mode=TwoWay}"
+                  ItemsSource="{Binding EnumValues}">
+            <ComboBox.ItemContainerTheme>
+                <ControlTheme TargetType="ComboBoxItem" BasedOn="{StaticResource {x:Type ComboBoxItem}}">
+                    <Setter Property="Tag" Value="{Binding .}" />
+                    <Setter Property="(localization:Translator.Key)"
+                            Value="{Binding ., Converter={converters:EnumToLocalizedStringConverter}}" />
+                </ControlTheme>
+            </ComboBox.ItemContainerTheme>
+            <ComboBox.ItemTemplate>
+                <DataTemplate>
+                    <TextBlock
+                        localization:Translator.Key="{Binding ., Converter={converters:EnumToLocalizedStringConverter}}" />
+                </DataTemplate>
+            </ComboBox.ItemTemplate>
+        </ComboBox>
+        <ItemsControl IsVisible="{Binding PickerIsIconButtons}"
+                      Name="iconButtonsPicker"
+                      ItemsSource="{Binding EnumValues}"
+                      HorizontalAlignment="Center">
+                <ItemsControl.Styles>
+                    <Style Selector="ContentPresenter:nth-last-child(1) ToggleButton">
+                        <Setter Property="CornerRadius" Value="0,4,4,0"/>
+                        <Setter Property="BorderThickness" Value="0,1,1,1"/>
+                    </Style>
+                    <Style Selector="ContentPresenter.first ToggleButton">
+                        <Setter Property="CornerRadius" Value="4,0,0,4"/>
+                        <Setter Property="BorderThickness" Value="1,1,0,1"/>
+                    </Style>
+                    <Style Selector="ToggleButton">
+                        <Setter Property="CornerRadius" Value="0"/>
+                        <Setter Property="BorderThickness" Value="0, 1"/>
+                    </Style>
+                    <Style Selector="ToggleButton:unchecked">
+                        <Setter Property="Background" Value="{DynamicResource ThemeControlLowBrush}"/>
+                    </Style>
+                </ItemsControl.Styles>
+            <ItemsControl.ItemsPanel>
+                <ItemsPanelTemplate>
+                    <StackPanel Orientation="Horizontal"
+                                HorizontalAlignment="Center" />
+                </ItemsPanelTemplate>
+            </ItemsControl.ItemsPanel>
+            <ItemsControl.ItemTemplate>
+                <DataTemplate>
+                    <ToggleButton Padding="4"
+                                  behaviours:ToggleGroupBehavior.GroupName="PickerMode"
+                                  behaviours:ToggleGroupBehavior.Value="{Binding .}"
+                                  behaviours:ToggleGroupBehavior.SelectedValue="{Binding DataContext.Value,
+                              RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay}"
+                                  localization:Translator.TooltipKey="{Binding ., Converter={converters:EnumToLocalizedStringConverter}}">
+
+                        <TextBlock FontSize="20" Classes="pixi-icon" Text="{Binding ., Converter={converters:EnumToIconConverter}}"/>
+                    </ToggleButton>
+                </DataTemplate>
+            </ItemsControl.ItemTemplate>
+
+        </ItemsControl>
+    </Panel>
+</UserControl>

+ 24 - 0
src/PixiEditor/Views/Tools/ToolSettings/Settings/EnumSettingView.axaml.cs

@@ -1,6 +1,10 @@
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
+using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 
 namespace PixiEditor.Views.Tools.ToolSettings.Settings;
 
@@ -10,5 +14,25 @@ public partial class EnumSettingView : UserControl
     {
         InitializeComponent();
     }
+
+    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnAttachedToVisualTree(e);
+        iconButtonsPicker.ContainerPrepared += IconButtonsPickerOnContainerPrepared;
+    }
+
+    override protected void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnDetachedFromVisualTree(e);
+        iconButtonsPicker.ContainerPrepared -= IconButtonsPickerOnContainerPrepared;
+    }
+
+    private void IconButtonsPickerOnContainerPrepared(object? sender, ContainerPreparedEventArgs e)
+    {
+        if (e.Index == 0)
+        {
+            e.Container.Classes.Add("first");
+        }
+    }
 }