Browse Source

Added error validation and improved mini string editor

Krzysztof Krysiński 5 months ago
parent
commit
adfb36bfd3

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 92190651911271f52a03fed63eb7ad2679b77430
+Subproject commit 848a8491a1ef9ccc47a6426d84902331b271fa53

+ 4 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/PropertyValueUpdated_ChangeInfo.cs

@@ -1,3 +1,6 @@
 namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 
-public record PropertyValueUpdated_ChangeInfo(Guid NodeId, string Property, object Value) : IChangeInfo;
+public record PropertyValueUpdated_ChangeInfo(Guid NodeId, string Property, object Value) : IChangeInfo
+{
+    public string? Errors { get; set; }
+}

+ 17 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ShaderNode.cs

@@ -27,7 +27,9 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
     public ShaderNode()
     {
         Background = CreateRenderInput("Background", "BACKGROUND");
-        ShaderCode = CreateInput("ShaderCode", "SHADER_CODE", "");
+        ShaderCode = CreateInput("ShaderCode", "SHADER_CODE", "")
+            .WithRules(validator => validator.Custom(ValidateShaderCode));
+
         paint = new Paint();
         Output.FirstInChain = null;
     }
@@ -60,7 +62,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
                 return;
             }
         }
-        else if(shader != null)
+        else if (shader != null)
         {
             Uniforms uniforms = GenerateUniforms(context);
             shader = shader.WithUpdatedUniforms(uniforms);
@@ -78,7 +80,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         uniforms.Add("iNormalizedTime", new Uniform("iNormalizedTime", (float)context.FrameTime.NormalizedTime));
         uniforms.Add("iFrame", new Uniform("iFrame", context.FrameTime.Frame));
 
-        if(Background.Value == null)
+        if (Background.Value == null)
         {
             lastImageShader?.Dispose();
             lastImageShader = null;
@@ -130,4 +132,16 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
     {
         return new ShaderNode();
     }
+
+    private ValidatorResult ValidateShaderCode(object? value)
+    {
+        if (value is string code)
+        {
+            var result = Shader.CreateFromString(code, out string errors);
+            result?.Dispose();
+            return new (string.IsNullOrWhiteSpace(errors), errors);
+        }
+
+        return new (false, "Shader code must be a string");
+    }
 }

+ 35 - 8
src/PixiEditor.ChangeableDocument/Changeables/Graph/PropertyValidator.cs

@@ -2,7 +2,7 @@
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
-public delegate (bool validationResult, object? closestValidValue) ValidateProperty(object? value);
+public delegate ValidatorResult ValidateProperty(object? value);
 
 public class PropertyValidator
 {
@@ -25,15 +25,21 @@ public class PropertyValidator
             if (value is T val)
             {
                 bool isValid = val.CompareTo(min) >= 0;
-                return (isValid, isValid ? val : GetReturnValue(val, min, adjust));
+                return new (isValid, isValid ? val : GetReturnValue(val, min, adjust));
             }
 
-            return (false, GetReturnValue(min, min, adjust));
+            return new (false, GetReturnValue(min, min, adjust));
         });
 
         return this;
     }
 
+    public PropertyValidator Custom(ValidateProperty rule)
+    {
+        Rules.Add(rule);
+        return this;
+    }
+
     private object? GetReturnValue<T>(T original, T min, Func<T, T>? fallback) where T : IComparable<T>
     {
         if (fallback != null)
@@ -44,25 +50,46 @@ public class PropertyValidator
         return min;
     }
 
-    public bool Validate(object? value)
+    public bool Validate(object? value, out string? errors)
     {
         object lastValue = value;
 
         foreach (var rule in Rules)
         {
-            var (isValid, toPass) = rule(lastValue);
-            lastValue = toPass;
-            if (!isValid)
+            var result = rule(lastValue);
+            lastValue = result.ClosestValidValue;
+            if (!result.IsValid)
             {
+                errors = result.ErrorMessage;
                 return false;
             }
         }
 
+        errors = null;
         return true;
     }
 
     public object? GetClosestValidValue(object? o)
     {
-        return Rules.Aggregate(o, (current, rule) => rule(current).closestValidValue);
+        return Rules.Aggregate(o, (current, rule) => rule(current).ClosestValidValue);
+    }
+}
+
+public record ValidatorResult
+{
+    public bool IsValid { get; }
+    public object? ClosestValidValue { get; }
+    public string? ErrorMessage { get; }
+
+    public ValidatorResult(bool isValid, string? errorMessage)
+    {
+        IsValid = isValid;
+        ErrorMessage = errorMessage;
+    }
+
+    public ValidatorResult(bool isValid, object? closestValidValue)
+    {
+        IsValid = isValid;
+        ClosestValidValue = closestValidValue;
     }
 }

+ 11 - 5
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/UpdateProperty_Change.cs

@@ -35,13 +35,19 @@ internal class UpdatePropertyValue_Change : Change
         var node = target.NodeGraph.Nodes.First(x => x.Id == _nodeId);
         var property = node.GetInputProperty(_propertyName);
 
+        List<IChangeInfo> changes = new();
+
         previousValue = GetValue(property);
-        if (!property.Validator.Validate(_value))
+        string errors = string.Empty;
+        if (!property.Validator.Validate(_value, out errors))
         {
-            _value = property.Validator.GetClosestValidValue(_value);
-            if (_value == previousValue)
+            if (string.IsNullOrEmpty(errors))
             {
-                ignoreInUndo = true;
+                _value = property.Validator.GetClosestValidValue(_value);
+                if (_value == previousValue)
+                {
+                    ignoreInUndo = true;
+                }
             }
 
             _value = SetValue(property, _value);
@@ -53,7 +59,7 @@ internal class UpdatePropertyValue_Change : Change
             ignoreInUndo = false;
         }
 
-        return new PropertyValueUpdated_ChangeInfo(_nodeId, _propertyName, _value);
+        return new PropertyValueUpdated_ChangeInfo(_nodeId, _propertyName, _value) { Errors = errors };
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)

BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll


BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll


+ 2 - 0
src/PixiEditor.UI.Common/Accents/Base.axaml

@@ -29,6 +29,7 @@
             <Color x:Key="ThemeBorderHighColor">#4F4F4F</Color>
 
             <Color x:Key="ErrorColor">#B00020</Color>
+            <Color x:Key="ErrorOnDarkColor">#FF0000</Color>
 
             <Color x:Key="GlyphColor">#444</Color>
             <Color x:Key="GlyphBackground">White</Color>
@@ -120,6 +121,7 @@
             <SolidColorBrush x:Key="NotificationCardBackgroundBrush" Color="{StaticResource NotificationCardBackgroundColor}" />
 
             <SolidColorBrush x:Key="ErrorBrush" Color="{StaticResource ErrorColor}" />
+            <SolidColorBrush x:Key="ErrorOnDarkBrush" Color="{StaticResource ErrorOnDarkColor}" />
             <SolidColorBrush x:Key="GlyphBrush" Color="{StaticResource GlyphColor}"/>
             <SolidColorBrush x:Key="ThumbBrush" Color="{StaticResource ThumbColor}"/>
             

+ 11 - 0
src/PixiEditor/Helpers/Converters/NotEmptyStringConverter.cs

@@ -0,0 +1,11 @@
+using System.Globalization;
+
+namespace PixiEditor.Helpers.Converters;
+
+internal class NotEmptyStringConverter : SingleInstanceConverter<NotEmptyStringConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        return !string.IsNullOrEmpty(value as string);
+    }
+}

+ 5 - 0
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -664,6 +664,11 @@ internal class DocumentUpdater
         NodeViewModel node = doc.StructureHelper.FindNode<NodeViewModel>(info.NodeId);
         var property = node.FindInputProperty(info.Property);
 
+        property.Errors = info.Errors;
+
+        if(info.Errors != null)
+            return;
+
         ProcessStructureMemberProperty(info, property);
         
         property.InternalSetValue(info.Value);

+ 1 - 1
src/PixiEditor/Styles/PixiEditor.Controls.axaml

@@ -23,7 +23,7 @@
         </ResourceDictionary>
     </Styles.Resources>
 
+    <StyleInclude Source="avares://PixiEditor/Styles/Templates/NodePropertyViewTemplate.axaml"/>
     <StyleInclude Source="avares://PixiEditor/Styles/PortingWipStyles.axaml"/>
     <StyleInclude Source="avares://PixiEditor/Styles/ToolPickerButton.Styles.axaml"/>
-    <StyleInclude Source="avares://PixiEditor/Styles/Templates/NodePropertyViewTemplate.axaml"/>
 </Styles>

+ 14 - 4
src/PixiEditor/Styles/Templates/NodePropertyViewTemplate.axaml

@@ -1,12 +1,15 @@
 <Styles xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:properties="clr-namespace:PixiEditor.Views.Nodes.Properties">
+        xmlns:properties="clr-namespace:PixiEditor.Views.Nodes.Properties"
+        xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+        xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters">
 
     <Style Selector="properties|NodePropertyView">
         <Setter Property="ClipToBounds" Value="False" />
         <Setter Property="Template">
             <ControlTemplate>
-                <Grid Margin="-5, 2" ColumnDefinitions="15, *, 15" MinHeight="18" IsVisible="{Binding DataContext.IsVisible, RelativeSource={RelativeSource TemplatedParent}}">
+                <Grid Margin="-5, 2" ColumnDefinitions="15, *, 15" MinHeight="18"
+                      IsVisible="{Binding DataContext.IsVisible, RelativeSource={RelativeSource TemplatedParent}}">
                     <properties:NodeSocket Name="PART_InputSocket"
                                            ClipToBounds="False"
                                            Node="{Binding DataContext.Node, RelativeSource={RelativeSource TemplatedParent}}"
@@ -17,9 +20,10 @@
                             <x:Boolean>True</x:Boolean>
                         </properties:NodeSocket.IsInput>
                     </properties:NodeSocket>
-                    <ContentPresenter Grid.Column="1" VerticalAlignment="Top" Content="{TemplateBinding Content}" />
+                    <ContentPresenter Grid.Column="1" Name="PART_Presenter"
+                                      VerticalAlignment="Top" Content="{TemplateBinding Content}" />
                     <properties:NodeSocket Name="PART_OutputSocket"
-                                           ClipToBounds="False" HorizontalAlignment="Right"  Grid.Column="2"
+                                           ClipToBounds="False" HorizontalAlignment="Right" Grid.Column="2"
                                            Node="{Binding DataContext.Node, RelativeSource={RelativeSource TemplatedParent}}"
                                            SocketBrush="{Binding DataContext.SocketBrush, RelativeSource={RelativeSource TemplatedParent}}"
                                            IsVisible="{Binding !DataContext.IsInput,RelativeSource={RelativeSource TemplatedParent}}"
@@ -32,4 +36,10 @@
             </ControlTemplate>
         </Setter>
     </Style>
+
+    <Style Selector="properties|NodePropertyView:has-errors ContentPresenter#PART_Presenter">
+        <Setter Property="Foreground" Value="{DynamicResource ErrorOnDarkBrush}" />
+        <Setter Property="(ui:Translator.TooltipKey)"
+                Value="{Binding DataContext.Errors, RelativeSource={RelativeSource TemplatedParent}}" />
+    </Style>
 </Styles>

+ 7 - 0
src/PixiEditor/ViewModels/Nodes/NodePropertyViewModel.cs

@@ -21,6 +21,7 @@ internal abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHand
     private bool isInput;
     private bool isFunc;
     private IBrush socketBrush;
+    private string errors = string.Empty;
 
     private ObservableCollection<INodePropertyHandler> connectedInputs = new();
     private INodePropertyHandler? connectedOutput;
@@ -114,6 +115,12 @@ internal abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHand
 
     public Type PropertyType { get; }
 
+    public string? Errors
+    {
+        get => errors;
+        set => SetProperty(ref errors, value);
+    }
+
     public NodePropertyViewModel(INodeHandler node, Type propertyType)
     {
         Node = node;

+ 39 - 0
src/PixiEditor/Views/Nodes/Properties/NodePropertyView.cs

@@ -1,17 +1,33 @@
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using PixiEditor.Models.Handlers;
 using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.Views.Nodes.Properties;
 
+[PseudoClasses(":has-errors")]
 public abstract class NodePropertyView : UserControl
 {
+    public static readonly StyledProperty<string> ErrorsProperty = AvaloniaProperty.Register<NodePropertyView, string>(
+        nameof(Errors));
+
+    public string Errors
+    {
+        get => GetValue(ErrorsProperty);
+        set => SetValue(ErrorsProperty, value);
+    }
+
     public NodeSocket InputSocket { get; private set; }
     public NodeSocket OutputSocket { get; private set; }
     protected override Type StyleKeyOverride => typeof(NodePropertyView);
 
+    static NodePropertyView()
+    {
+        ErrorsProperty.Changed.Subscribe(OnErrorsChanged);
+    }
+
     protected void SetValue(object value)
     {
         if (DataContext is NodePropertyViewModel viewModel)
@@ -20,6 +36,21 @@ public abstract class NodePropertyView : UserControl
         }
     }
 
+    protected override void OnDataContextChanged(EventArgs e)
+    {
+        base.OnDataContextChanged(e);
+        if (DataContext is NodePropertyViewModel propertyHandler)
+        {
+            propertyHandler.PropertyChanged += (sender, args) =>
+            {
+                if (args.PropertyName == nameof(NodePropertyViewModel.Errors))
+                {
+                    Errors = propertyHandler.Errors;
+                }
+            };
+        }
+    }
+
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
     {
         base.OnApplyTemplate(e);
@@ -70,6 +101,14 @@ public abstract class NodePropertyView : UserControl
             OutputSocket.IsVisible = false;
         }
     }
+
+    private static void OnErrorsChanged(AvaloniaPropertyChangedEventArgs<string> args)
+    {
+        if (args.Sender is NodePropertyView view)
+        {
+            view.PseudoClasses.Set(":has-errors", args.NewValue.HasValue && !string.IsNullOrEmpty(args.NewValue.Value));
+        }
+    }
 }
 
 public abstract class NodePropertyView<T> : NodePropertyView

+ 36 - 15
src/PixiEditor/Views/Nodes/Properties/StringPropertyView.axaml

@@ -30,7 +30,10 @@
             </TextBox>
         </DockPanel>
         <Popup IsOpen="{Binding ElementName=bigModeToggle, Path=IsChecked, Mode=TwoWay}"
-               Placement="Bottom"
+               Placement="AnchorAndGravity"
+               PlacementAnchor="Top"
+               VerticalOffset="10"
+               PlacementGravity="Bottom"
                IsLightDismissEnabled="True"
                Opened="Popup_OnOpened"
                PlacementTarget="{Binding ElementName=bigModeToggle}">
@@ -50,20 +53,38 @@
                                 Content="{DynamicResource icon-folder}" FontSize="20" />
                     </StackPanel>
                 </Border>
-                <TextBox
-                    CornerRadius="0 0 5 5"
-                    Name="bigTextBox"
-                    Width="500"
-                    Height="600"
-                    AcceptsReturn="True"
-                    AcceptsTab="True"
-                    PointerWheelChanged="InputElement_OnPointerWheelChanged"
-                    Text="{Binding StringValue, Mode=TwoWay}"
-                    IsVisible="{Binding ElementName=bigModeToggle, Path=IsChecked}">
-                    <Interaction.Behaviors>
-                        <behaviours:GlobalShortcutFocusBehavior />
-                    </Interaction.Behaviors>
-                </TextBox>
+                <Grid>
+                    <Grid.RowDefinitions>
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="70" />
+                    </Grid.RowDefinitions>
+                    <TextBox
+                        CornerRadius="0 0 5 5"
+                        Name="bigTextBox"
+                        Width="500"
+                        Height="600"
+                        AcceptsReturn="True"
+                        AcceptsTab="True"
+                        PointerWheelChanged="InputElement_OnPointerWheelChanged"
+                        Text="{Binding StringValue, Mode=TwoWay}"
+                        IsVisible="{Binding ElementName=bigModeToggle, Path=IsChecked}">
+                        <Interaction.Behaviors>
+                            <behaviours:GlobalShortcutFocusBehavior />
+                        </Interaction.Behaviors>
+                    </TextBox>
+                    <Border
+                        CornerRadius="0 0 5 5"
+                        BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                        IsVisible="{Binding Errors, Converter={converters:NotEmptyStringConverter}}"
+                        Background="{DynamicResource ThemeBackgroundBrush}" Grid.Row="1">
+                        <ScrollViewer PointerWheelChanged="InputElement_OnPointerWheelChanged" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
+                        <TextBlock
+                            Foreground="{DynamicResource ErrorOnDarkBrush}"
+                            TextWrapping="Wrap"
+                            Text="{Binding Errors}" />
+                        </ScrollViewer>
+                    </Border>
+                </Grid>
             </DockPanel>
         </Popup>
     </Grid>