ソースを参照

Improved grabber undo wip

Krzysztof Krysiński 1 ヶ月 前
コミット
638c9c7d32

+ 60 - 9
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/UpdateProperty_Change.cs

@@ -5,14 +5,14 @@ using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 
 namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
 
-internal class UpdatePropertyValue_Change : Change
+internal class UpdatePropertyValue_Change : InterruptableUpdateableChange
 {
     private readonly Guid _nodeId;
     private readonly string _propertyName;
     private object? _value;
     private object? previousValue;
 
-    [GenerateMakeChangeAction]
+    [GenerateUpdateableChangeActions]
     public UpdatePropertyValue_Change(Guid nodeId, string property, object? value)
     {
         _nodeId = nodeId;
@@ -24,14 +24,28 @@ internal class UpdatePropertyValue_Change : Change
     {
         if (target.TryFindNode<Node>(_nodeId, out var node))
         {
-            return node.HasInputProperty(_propertyName);
+            var property = node.GetInputProperty(_propertyName);
+            if (property == null) return false;
+
+            previousValue = GetValue(property);
+            if (previousValue is ShaderExpressionVariable expr)
+            {
+                previousValue = expr.GetConstant();
+            }
+
+            return true;
         }
 
         return false;
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
-        out bool ignoreInUndo)
+    [UpdateChangeMethod]
+    public void UpdateValue(object? value)
+    {
+        _value = value;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
         var node = target.NodeGraph.Nodes.First(x => x.Id == _nodeId);
         var property = node.GetInputProperty(_propertyName);
@@ -39,12 +53,49 @@ internal class UpdatePropertyValue_Change : Change
         int inputsHash = CalculateInputsHash(node);
         int outputsHash = CalculateOutputsHash(node);
 
-        previousValue = GetValue(property);
-        if (previousValue is ShaderExpressionVariable expr)
+        string errors = string.Empty;
+        if (!property.Validator.Validate(_value, out errors))
+        {
+            if (string.IsNullOrEmpty(errors))
+            {
+                _value = property.Validator.GetClosestValidValue(_value);
+            }
+
+            _value = SetValue(property, _value);
+        }
+        else
+        {
+            _value = SetValue(property, _value);
+        }
+
+        List<IChangeInfo> changes = new();
+        changes.Add(new PropertyValueUpdated_ChangeInfo(_nodeId, _propertyName, _value) { Errors = errors });
+
+        int newInputsHash = CalculateInputsHash(node);
+        int newOutputsHash = CalculateOutputsHash(node);
+
+        if (inputsHash != newInputsHash)
         {
-            previousValue = expr.GetConstant();
+            changes.Add(NodeInputsChanged_ChangeInfo.FromNode(node));
+        }
+
+        if (outputsHash != newOutputsHash)
+        {
+            changes.Add(NodeOutputsChanged_ChangeInfo.FromNode(node));
         }
 
+        return changes;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        var node = target.NodeGraph.Nodes.First(x => x.Id == _nodeId);
+        var property = node.GetInputProperty(_propertyName);
+
+        int inputsHash = CalculateInputsHash(node);
+        int outputsHash = CalculateOutputsHash(node);
+
         string errors = string.Empty;
         if (!property.Validator.Validate(_value, out errors))
         {
@@ -172,6 +223,6 @@ internal class UpdatePropertyValue_Change : Change
     public override bool IsMergeableWith(Change other)
     {
         return other is UpdatePropertyValue_Change change && change._nodeId == _nodeId &&
-               change._propertyName == _propertyName;
+               change._propertyName == _propertyName && _value == change._value;
     }
 }

+ 16 - 0
src/PixiEditor.UI.Common/Controls/NumberInput.cs

@@ -42,6 +42,15 @@ public partial class NumberInput : TextBox
     public static readonly StyledProperty<bool> EnableGrabberProperty = AvaloniaProperty.Register<NumberInput, bool>(
         nameof(EnableGrabber), true);
 
+    public static readonly StyledProperty<bool> DraggingGrabberProperty = AvaloniaProperty.Register<NumberInput, bool>(
+        nameof(DraggingGrabber));
+
+    public bool DraggingGrabber
+    {
+        get => GetValue(DraggingGrabberProperty);
+        private set => SetValue(DraggingGrabberProperty, value);
+    }
+
     public bool EnableGrabber
     {
         get => GetValue(EnableGrabberProperty);
@@ -211,6 +220,7 @@ public partial class NumberInput : TextBox
 
         grabber.PointerPressed += GrabberPressed;
         grabber.PointerMoved += GrabberMoved;
+        grabber.PointerReleased += GrabberReleased;
 
         return grabber;
     }
@@ -220,6 +230,7 @@ public partial class NumberInput : TextBox
         e.Pointer.Capture(leftGrabber);
         _pressedValue = Value;
         _pressedRelativeX = e.GetPosition(this).X;
+        DraggingGrabber = true;
         e.Handled = true;
     }
 
@@ -240,6 +251,11 @@ public partial class NumberInput : TextBox
         }
     }
 
+    private void GrabberReleased(object sender, PointerReleasedEventArgs e)
+    {
+        DraggingGrabber = false;
+    }
+
     private void BindTextBoxBehavior(TextBoxFocusBehavior behavior)
     {
         Binding focusNextBinding = new Binding(nameof(FocusNext)) { Source = this, Mode = BindingMode.OneWay };

+ 11 - 1
src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs

@@ -228,7 +228,17 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
 
     public void UpdatePropertyValue(INodeHandler node, string property, object? value)
     {
-        Internals.ActionAccumulator.AddFinishedActions(new UpdatePropertyValue_Action(node.Id, property, value));
+        Internals.ActionAccumulator.AddFinishedActions(new UpdatePropertyValue_Action(node.Id, property, value), new EndUpdatePropertyValue_Action());
+    }
+
+    public void BeginUpdatePropertyValue(INodeHandler node, string property, object value)
+    {
+        Internals.ActionAccumulator.AddActions(new UpdatePropertyValue_Action(node.Id, property, value));
+    }
+
+    public void EndUpdatePropertyValue()
+    {
+        Internals.ActionAccumulator.AddFinishedActions(new EndUpdatePropertyValue_Action());
     }
 
     public void RequestUpdateComputedPropertyValue(INodePropertyHandler property)

+ 25 - 1
src/PixiEditor/ViewModels/Nodes/NodePropertyViewModel.cs

@@ -19,6 +19,7 @@ internal abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHand
     private bool isFunc;
     private IBrush socketBrush;
     private string errors = string.Empty;
+    private bool mergeChanges = false;
 
     private object computedValue;
 
@@ -39,7 +40,15 @@ internal abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHand
         set
         {
             var oldValue = _value;
-            ViewModelMain.Current.NodeGraphManager.UpdatePropertyValue((node, PropertyName, value));
+            if (MergeChanges)
+            {
+                ViewModelMain.Current.NodeGraphManager.BeginUpdatePropertyValue((node, PropertyName, value));
+            }
+            else
+            {
+                ViewModelMain.Current.NodeGraphManager.UpdatePropertyValue((node, PropertyName, value));
+            }
+
             if (SetProperty(ref _value, value))
             {
                 ValueChanged?.Invoke(this, new NodePropertyValueChangedArgs(oldValue, value));
@@ -47,6 +56,21 @@ internal abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHand
         }
     }
 
+    public bool MergeChanges
+    {
+        get => mergeChanges;
+        set
+        {
+            if (SetProperty(ref mergeChanges, value))
+            {
+                if (!value)
+                {
+                    ViewModelMain.Current.NodeGraphManager.EndUpdatePropertyValue();
+                }
+            }
+        }
+    }
+
     public object ComputedValue
     {
         get

+ 82 - 41
src/PixiEditor/ViewModels/Nodes/Properties/ColorMatrixPropertyViewModel.cs

@@ -1,9 +1,11 @@
-using Drawie.Numerics;
+using System.ComponentModel;
+using Drawie.Numerics;
 
 namespace PixiEditor.ViewModels.Nodes.Properties;
 
 internal class ColorMatrixPropertyViewModel : NodePropertyViewModel<ColorMatrix>
 {
+    private bool blockUpdates = false;
     public ColorMatrixPropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
     {
     }
@@ -11,160 +13,199 @@ internal class ColorMatrixPropertyViewModel : NodePropertyViewModel<ColorMatrix>
     public float M11
     {
         get => Value.M11;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (value, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M12
     {
         get => Value.M12;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, value, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M13
     {
         get => Value.M13;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, value, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M14
     {
         get => Value.M14;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, value, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M15
     {
         get => Value.M15;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, value), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M21
     {
         get => Value.M21;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (value, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M22
     {
         get => Value.M22;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, value, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M23
     {
         get => Value.M23;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, M22, value, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M24
     {
         get => Value.M24;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, M22, M23, value, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M25
     {
         get => Value.M25;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, value),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M31
     {
         get => Value.M31;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (value, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (value, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M32
     {
         get => Value.M32;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, value, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, value, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M33
     {
         get => Value.M33;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, value, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, value, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M34
     {
         get => Value.M34;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, value, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, value, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M35
     {
         get => Value.M35;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, value), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, value), (M41, M42, M43, M44, M45)));
     }
 
     public float M41
     {
         get => Value.M41;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (value, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (value, M42, M43, M44, M45)));
     }
 
     public float M42
     {
         get => Value.M42;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, value, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, value, M43, M44, M45)));
     }
 
     public float M43
     {
         get => Value.M43;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, value, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, value, M44, M45)));
     }
 
     public float M44
     {
         get => Value.M44;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, value, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, value, M45)));
     }
 
     public float M45
     {
         get => Value.M45;
-        set => Value = new ColorMatrix(
+        set => UpdateValue(new ColorMatrix(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, value));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, value)));
+    }
+
+    private void UpdateValue(ColorMatrix value)
+    {
+        if (blockUpdates)
+            return;
+
+        Value = value;
+    }
+
+    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
+    {
+        base.OnPropertyChanged(e);
+
+        blockUpdates = true;
+        if (e.PropertyName == nameof(Value))
+        {
+            OnPropertyChanged(nameof(M11));
+            OnPropertyChanged(nameof(M12));
+            OnPropertyChanged(nameof(M13));
+            OnPropertyChanged(nameof(M14));
+            OnPropertyChanged(nameof(M15));
+            OnPropertyChanged(nameof(M21));
+            OnPropertyChanged(nameof(M22));
+            OnPropertyChanged(nameof(M23));
+            OnPropertyChanged(nameof(M24));
+            OnPropertyChanged(nameof(M25));
+            OnPropertyChanged(nameof(M31));
+            OnPropertyChanged(nameof(M32));
+            OnPropertyChanged(nameof(M33));
+            OnPropertyChanged(nameof(M34));
+            OnPropertyChanged(nameof(M35));
+            OnPropertyChanged(nameof(M41));
+            OnPropertyChanged(nameof(M42));
+            OnPropertyChanged(nameof(M43));
+            OnPropertyChanged(nameof(M44));
+            OnPropertyChanged(nameof(M45));
+        }
+        blockUpdates = false;
     }
 }

+ 66 - 11
src/PixiEditor/ViewModels/Nodes/Properties/KernelPropertyViewModel.cs

@@ -8,9 +8,11 @@ namespace PixiEditor.ViewModels.Nodes.Properties;
 internal class KernelPropertyViewModel : NodePropertyViewModel<Kernel?>
 {
     public ObservableCollection<KernelVmReference> ReferenceCollections { get; }
-    
+
     public RelayCommand<int> AdjustSizeCommand { get; }
-    
+
+    private bool blockUpdates = false;
+
     public KernelPropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
     {
         ReferenceCollections = new ObservableCollection<KernelVmReference>();
@@ -26,7 +28,7 @@ internal class KernelPropertyViewModel : NodePropertyViewModel<Kernel?>
     }
 
     public int Width => Value.Width;
-    
+
     public int Height => Value.Height;
 
     public float Sum => Value.Sum;
@@ -35,34 +37,87 @@ internal class KernelPropertyViewModel : NodePropertyViewModel<Kernel?>
     {
         if (e.PropertyName != nameof(Value) || Value == null)
             return;
+        blockUpdates = true;
+
+        int requiredCount = Value.Height * Value.Width;
 
-        ReferenceCollections.Clear();
-        
-        for (int y = -Value.RadiusY; y <= Value.RadiusY; y++)
+        if (ReferenceCollections.Count != requiredCount)
         {
-            for (int x = -Value.RadiusX; x <= Value.RadiusX; x++)
+            ReferenceCollections.Clear();
+            for (int y = -Value.RadiusY; y <= Value.RadiusY; y++)
             {
-                ReferenceCollections.Add(new KernelVmReference(this, x, y));
+                for (int x = -Value.RadiusX; x <= Value.RadiusX; x++)
+                {
+                    if (ReferenceCollections.Count < requiredCount)
+                    {
+                        ReferenceCollections.Add(new KernelVmReference(this, x, y));
+                    }
+                    else
+                    {
+                        break;
+                    }
+                }
             }
         }
-        
+
         OnPropertyChanged(nameof(Width));
         OnPropertyChanged(nameof(Height));
         OnPropertyChanged(nameof(Sum));
+        OnPropertyChanged(nameof(ReferenceCollections));
+
+        for (int i = 0; i < ReferenceCollections.Count; i++)
+        {
+            var reference = ReferenceCollections[i];
+            reference.ValueChanged();
+        }
+
+        blockUpdates = false;
     }
 
     public class KernelVmReference(KernelPropertyViewModel viewModel, int x, int y) : PixiObservableObject
     {
+        public bool MergeChanges
+        {
+            get
+            {
+                return viewModel.MergeChanges;
+            }
+            set
+            {
+                viewModel.MergeChanges = value;
+                OnPropertyChanged();
+            }
+        }
+
         public float Value
         {
             get => viewModel.Value[x, y];
             set
             {
-                viewModel.Value[x, y] = value;
-                ViewModelMain.Current.NodeGraphManager.UpdatePropertyValue((viewModel.Node, viewModel.PropertyName, viewModel.Value));
+                if (viewModel.blockUpdates)
+                    return;
+
+                var newVal = viewModel.Value.Clone() as Kernel;
+                newVal[x, y] = value;
+                if (MergeChanges)
+                {
+                    ViewModelMain.Current.NodeGraphManager.BeginUpdatePropertyValue((viewModel.Node,
+                        viewModel.PropertyName, newVal));
+                }
+                else
+                {
+                    ViewModelMain.Current.NodeGraphManager.UpdatePropertyValue((viewModel.Node, viewModel.PropertyName,
+                        newVal));
+                }
+
                 viewModel.OnPropertyChanged(nameof(Sum));
                 OnPropertyChanged();
             }
         }
+
+        public void ValueChanged()
+        {
+            OnPropertyChanged(nameof(Value));
+        }
     }
 }

+ 85 - 41
src/PixiEditor/ViewModels/Nodes/Properties/Matrix4x5FPropertyViewModel.cs

@@ -1,9 +1,11 @@
-using Drawie.Numerics;
+using System.ComponentModel;
+using Drawie.Numerics;
 
 namespace PixiEditor.ViewModels.Nodes.Properties;
 
 internal class Matrix4x5FPropertyViewModel : NodePropertyViewModel<Matrix4x5F>
 {
+    private bool blockUpdates = false;
     public Matrix4x5FPropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
     {
     }
@@ -11,160 +13,202 @@ internal class Matrix4x5FPropertyViewModel : NodePropertyViewModel<Matrix4x5F>
     public float M11
     {
         get => Value.M11;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (value, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M12
     {
         get => Value.M12;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, value, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M13
     {
         get => Value.M13;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, value, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M14
     {
         get => Value.M14;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, value, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M15
     {
         get => Value.M15;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, value), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M21
     {
         get => Value.M21;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (value, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M22
     {
         get => Value.M22;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, value, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M23
     {
         get => Value.M23;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, M22, value, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M24
     {
         get => Value.M24;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, M22, M23, value, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M25
     {
         get => Value.M25;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, value),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M31
     {
         get => Value.M31;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (value, M32, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (value, M32, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M32
     {
         get => Value.M32;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, value, M33, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, value, M33, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M33
     {
         get => Value.M33;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, value, M34, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, value, M34, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M34
     {
         get => Value.M34;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, value, M35), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, value, M35), (M41, M42, M43, M44, M45)));
     }
 
     public float M35
     {
         get => Value.M35;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, value), (M41, M42, M43, M44, M45));
+            (M31, M32, M33, M34, value), (M41, M42, M43, M44, M45)));
     }
 
     public float M41
     {
         get => Value.M41;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (value, M42, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (value, M42, M43, M44, M45)));
     }
 
     public float M42
     {
         get => Value.M42;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, value, M43, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, value, M43, M44, M45)));
     }
 
     public float M43
     {
         get => Value.M43;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, value, M44, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, value, M44, M45)));
     }
 
     public float M44
     {
         get => Value.M44;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, value, M45));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, value, M45)));
     }
 
     public float M45
     {
         get => Value.M45;
-        set => Value = new Matrix4x5F(
+        set => UpdateValue(new Matrix4x5F(
             (M11, M12, M13, M14, M15), (M21, M22, M23, M24, M25),
-            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, value));
+            (M31, M32, M33, M34, M35), (M41, M42, M43, M44, value)));
+    }
+
+    private void UpdateValue(Matrix4x5F newValue)
+    {
+        if (blockUpdates)
+            return;
+
+        Value = newValue;
+    }
+
+    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
+    {
+        base.OnPropertyChanged(e);
+        if (blockUpdates)
+            return;
+        blockUpdates = true;
+
+        if (e.PropertyName == nameof(Value))
+        {
+            OnPropertyChanged(nameof(M11));
+            OnPropertyChanged(nameof(M12));
+            OnPropertyChanged(nameof(M13));
+            OnPropertyChanged(nameof(M14));
+            OnPropertyChanged(nameof(M15));
+            OnPropertyChanged(nameof(M21));
+            OnPropertyChanged(nameof(M22));
+            OnPropertyChanged(nameof(M23));
+            OnPropertyChanged(nameof(M24));
+            OnPropertyChanged(nameof(M25));
+            OnPropertyChanged(nameof(M31));
+            OnPropertyChanged(nameof(M32));
+            OnPropertyChanged(nameof(M33));
+            OnPropertyChanged(nameof(M34));
+            OnPropertyChanged(nameof(M35));
+            OnPropertyChanged(nameof(M41));
+            OnPropertyChanged(nameof(M42));
+            OnPropertyChanged(nameof(M43));
+            OnPropertyChanged(nameof(M44));
+            OnPropertyChanged(nameof(M45));
+        }
+
+        blockUpdates = false;
     }
 }

+ 24 - 6
src/PixiEditor/ViewModels/Nodes/Properties/Vec3DPropertyViewModel.cs

@@ -5,6 +5,7 @@ namespace PixiEditor.ViewModels.Nodes.Properties;
 
 internal class Vec3DPropertyViewModel : NodePropertyViewModel<Vec3D>
 {
+    private bool updateBlocker = false;
     public Vec3DPropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
     {
         PropertyChanged += OnPropertyChanged;
@@ -16,27 +17,44 @@ internal class Vec3DPropertyViewModel : NodePropertyViewModel<Vec3D>
         {
             return;
         }
-        
+
+        updateBlocker = true;
         OnPropertyChanged(nameof(XValue));
         OnPropertyChanged(nameof(YValue));
         OnPropertyChanged(nameof(ZValue));
+        updateBlocker = false;
     }
 
     public double XValue
     {
         get => Value.X;
-        set => Value = new Vec3D(value, Value.Y, Value.Z);
+        set
+        {
+            if (updateBlocker)
+                return;
+            Value = new Vec3D(value, Value.Y, Value.Z);
+        }
     }
-    
+
     public double YValue
     {
         get => Value.Y;
-        set => Value = new Vec3D(Value.X, value, Value.Z);
+        set
+        {
+            if (updateBlocker)
+                return;
+            Value = new Vec3D(Value.X, value, Value.Z);
+        }
     }
-    
+
     public double ZValue
     {
         get => Value.Z;
-        set => Value = new Vec3D(Value.X, Value.Y, value);
+        set
+        {
+            if (updateBlocker)
+                return;
+            Value = new Vec3D(Value.X, Value.Y, value);
+        }
     }
 }

+ 13 - 0
src/PixiEditor/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs

@@ -85,6 +85,19 @@ internal class NodeGraphManagerViewModel : SubViewModel<ViewModelMain>
             args.value);
     }
 
+    [Command.Internal("PixiEditor.NodeGraph.BeginUpdateValue", AnalyticsTrack = true)]
+    public void BeginUpdatePropertyValue((INodeHandler node, string property, object value) args)
+    {
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.BeginUpdatePropertyValue(args.node, args.property,
+            args.value);
+    }
+
+    [Command.Internal("PixiEditor.NodeGraph.EndUpdateValue", AnalyticsTrack = true)]
+    public void EndUpdatePropertyValue()
+    {
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.EndUpdatePropertyValue();
+    }
+
     [Command.Internal("PixiEditor.NodeGraph.EndChangeNodePos")]
     public void EndChangeNodePos()
     {

+ 24 - 20
src/PixiEditor/Views/Nodes/Properties/ColorMatrixPropertyView.axaml

@@ -6,8 +6,12 @@
                              xmlns:ui="clr-namespace:PixiEditor.UI.Common.Localization;assembly=PixiEditor.UI.Common"
                              xmlns:input="clr-namespace:PixiEditor.UI.Common.Controls;assembly=PixiEditor.UI.Common"
                              xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+                             xmlns:properties1="clr-namespace:PixiEditor.ViewModels.Nodes.Properties"
                              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
                              x:Class="PixiEditor.Views.Nodes.Properties.ColorMatrixPropertyView">
+    <Design.DataContext>
+        <properties1:ColorMatrixPropertyViewModel />
+    </Design.DataContext>
     <StackPanel HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
         <TextBlock ui:Translator.Key="{Binding DisplayName}" />
         <Grid IsVisible="{Binding ShowInputField}" ColumnDefinitions="Auto,*,*,*,*,*" RowDefinitions="Auto, Auto, Auto, Auto, Auto">
@@ -24,26 +28,26 @@
             <TextBlock Grid.Row="0" Grid.Column="4" Text="A" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" TextAlignment="Center" />
             <TextBlock Grid.Row="0" Grid.Column="5" Text="+" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" TextAlignment="Center" />
             
-            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M11, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M12, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M13, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M14, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M15, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M21, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M22, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M23, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M24, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M25, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M31, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M32, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M33, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M34, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M35, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M41, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M42, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M43, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M44, Mode=TwoWay}" />
-            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M45, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="1" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M11, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="2" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M12, Mode=TwoWay}"  />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="3" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M13, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="4" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M14, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="5" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M15, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="1" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M21, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="2" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M22, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="3" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M23, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="4" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M24, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="5" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M25, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="1" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M31, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="2" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M32, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="3" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M33, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="4" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M34, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="5" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M35, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="1" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M41, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="2" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M42, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="3" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M43, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="4" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M44, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="5" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M45, Mode=TwoWay}" />
         </Grid>
     </StackPanel>
 </properties:NodePropertyView>

+ 1 - 0
src/PixiEditor/Views/Nodes/Properties/DoublePropertyView.axaml

@@ -23,6 +23,7 @@
             <TextBlock VerticalAlignment="Center" DockPanel.Dock="Left" ui:Translator.Key="{Binding DisplayName}" />
             <Panel HorizontalAlignment="Right" IsVisible="{Binding ShowInputField}">
                 <controls:NumberInput EnableScrollChange="False" Name="input"
+                                      DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}"
                                    MinWidth="100" Decimals="6"
                                    IsVisible="{Binding NumberPickerMode,
                                 Converter={converters:EnumBooleanConverter}, ConverterParameter=NumberInput}"

+ 5 - 0
src/PixiEditor/Views/Nodes/Properties/Int32PropertyView.axaml

@@ -6,11 +6,16 @@
                              xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
                              xmlns:controls="clr-namespace:PixiEditor.UI.Common.Controls;assembly=PixiEditor.UI.Common"
                              xmlns:localization="clr-namespace:PixiEditor.UI.Common.Localization;assembly=PixiEditor.UI.Common"
+                             xmlns:properties1="clr-namespace:PixiEditor.ViewModels.Nodes.Properties"
                              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
                              x:Class="PixiEditor.Views.Nodes.Properties.Int32PropertyView">
+    <Design.DataContext>
+        <properties1:Int32PropertyViewModel />
+    </Design.DataContext>
     <Grid HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
         <TextBlock VerticalAlignment="Center" localization:Translator.Key="{Binding DisplayName}"/>
         <controls:NumberInput EnableScrollChange="False" Decimals="0"
+                              DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}"
             HorizontalAlignment="Right" MinWidth="100" IsVisible="{Binding ShowInputField}" Value="{Binding Value, Mode=TwoWay}" />
     </Grid>
 </properties:NodePropertyView>

+ 5 - 1
src/PixiEditor/Views/Nodes/Properties/KernelPropertyView.axaml

@@ -8,8 +8,12 @@
                              xmlns:input="clr-namespace:PixiEditor.Views.Input"
                              xmlns:system="clr-namespace:System;assembly=System.Runtime"
                              xmlns:controls="clr-namespace:PixiEditor.UI.Common.Controls;assembly=PixiEditor.UI.Common"
+                             xmlns:properties1="clr-namespace:PixiEditor.ViewModels.Nodes.Properties"
                              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
                              x:Class="PixiEditor.Views.Nodes.Properties.KernelPropertyView">
+    <Design.DataContext>
+        <properties1:KernelPropertyViewModel />
+    </Design.DataContext>
 
     <StackPanel Margin="0,2">
         <Grid ColumnDefinitions="*,*,*" Margin="0,0,0,2">
@@ -39,7 +43,7 @@
         <ItemsControl ItemsSource="{Binding ReferenceCollections}" Margin="0,1">
             <ItemsControl.ItemTemplate>
                 <DataTemplate>
-                    <controls:NumberInput EnableScrollChange="False" Value="{Binding Value, Mode=TwoWay}" Decimals="4" />
+                    <controls:NumberInput DraggingGrabber="{Binding Path=MergeChanges, Mode=OneWayToSource}" EnableScrollChange="False" Value="{Binding Value, Mode=TwoWay}" Decimals="4" />
                 </DataTemplate>
             </ItemsControl.ItemTemplate>
             <ItemsControl.ItemsPanel>

+ 24 - 20
src/PixiEditor/Views/Nodes/Properties/Matrix4x5FPropertyView.axaml

@@ -6,8 +6,12 @@
                              xmlns:input="clr-namespace:PixiEditor.UI.Common.Controls;assembly=PixiEditor.UI.Common"
                              xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
                              xmlns:localization="clr-namespace:PixiEditor.UI.Common.Localization;assembly=PixiEditor.UI.Common"
+                             xmlns:properties1="clr-namespace:PixiEditor.ViewModels.Nodes.Properties"
                              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
                              x:Class="PixiEditor.Views.Nodes.Properties.Matrix4x5FPropertyView">
+    <Design.DataContext>
+        <properties1:Matrix4x5FPropertyViewModel />
+    </Design.DataContext>
     <StackPanel HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
         <TextBlock localization:Translator.Key="{Binding DisplayName}" />
         <Grid IsVisible="{Binding ShowInputField}" ColumnDefinitions="Auto,*,*,*,*,*" RowDefinitions="Auto, Auto, Auto, Auto, Auto">
@@ -22,26 +26,26 @@
             <TextBlock Grid.Row="0" Grid.Column="4" Text="4" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" />
             <TextBlock Grid.Row="0" Grid.Column="5" Text="5" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0" />
             
-            <input:NumberInput Grid.Row="1" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M11, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="1" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M12, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="1" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M13, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="1" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M14, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="1" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M15, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="2" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M21, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="2" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M22, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="2" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M23, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="2" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M24, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="2" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M25, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="3" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M31, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="3" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M32, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="3" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M33, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="3" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M34, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="3" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M35, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="4" Grid.Column="1" IsVisible="{Binding IsInput}" Value="{Binding M41, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="4" Grid.Column="2" IsVisible="{Binding IsInput}" Value="{Binding M42, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="4" Grid.Column="3" IsVisible="{Binding IsInput}" Value="{Binding M43, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="4" Grid.Column="4" IsVisible="{Binding IsInput}" Value="{Binding M44, Mode=TwoWay}" />
-            <input:NumberInput Grid.Row="4" Grid.Column="5" IsVisible="{Binding IsInput}" Value="{Binding M45, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="1" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M11, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="2" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M12, Mode=TwoWay}"  />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="3" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M13, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="4" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M14, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="1" Grid.Column="5" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M15, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="1" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M21, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="2" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M22, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="3" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M23, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="4" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M24, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="2" Grid.Column="5" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M25, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="1" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M31, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="2" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M32, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="3" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M33, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="4" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M34, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="3" Grid.Column="5" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M35, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="1" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M41, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="2" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M42, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="3" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M43, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="4" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M44, Mode=TwoWay}" />
+            <input:NumberInput EnableScrollChange="False" Grid.Row="4" Grid.Column="5" IsVisible="{Binding IsInput}" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding M45, Mode=TwoWay}" />
         </Grid>
     </StackPanel>
 </properties:NodePropertyView>

+ 7 - 3
src/PixiEditor/Views/Nodes/Properties/Vec3DPropertyView.axaml

@@ -7,14 +7,18 @@
                              xmlns:ui="clr-namespace:PixiEditor.UI.Common.Localization;assembly=PixiEditor.UI.Common"
                              xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
                              xmlns:controls="clr-namespace:PixiEditor.UI.Common.Controls;assembly=PixiEditor.UI.Common"
+                             xmlns:properties1="clr-namespace:PixiEditor.ViewModels.Nodes.Properties"
                              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
                              x:Class="PixiEditor.Views.Nodes.Properties.Vec3DPropertyView">
+    <Design.DataContext>
+        <properties1:Vec3DPropertyViewModel />
+    </Design.DataContext>
     <StackPanel HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
         <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
         <StackPanel IsVisible="{Binding ShowInputField}">
-            <controls:NumberInput EnableScrollChange="False" MinWidth="100" Value="{Binding XValue, Mode=TwoWay}" Margin="0,2" />
-            <controls:NumberInput EnableScrollChange="False" MinWidth="100" Value="{Binding YValue, Mode=TwoWay}" Margin="0,2" />
-            <controls:NumberInput EnableScrollChange="False" MinWidth="100" Value="{Binding ZValue, Mode=TwoWay}" Margin="0,2" />
+            <controls:NumberInput EnableScrollChange="False" MinWidth="100" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding XValue, Mode=TwoWay}" Margin="0,2" />
+            <controls:NumberInput EnableScrollChange="False" MinWidth="100" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding YValue, Mode=TwoWay}" Margin="0,2" />
+            <controls:NumberInput EnableScrollChange="False" MinWidth="100" DraggingGrabber="{Binding MergeChanges, Mode=OneWayToSource}" Value="{Binding ZValue, Mode=TwoWay}" Margin="0,2" />
         </StackPanel>
     </StackPanel>
 </properties:NodePropertyView>