瀏覽代碼

Merge branch 'master' into development

flabbet 9 月之前
父節點
當前提交
8ed1d61850
共有 34 個文件被更改,包括 326 次插入112 次删除
  1. 1 1
      src/Drawie
  2. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  3. 10 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs
  4. 17 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs
  5. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  6. 2 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs
  7. 17 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  8. 3 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs
  9. 3 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  10. 58 22
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodePosition_UpdateableChange.cs
  11. 25 13
      src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs
  12. 10 2
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs
  13. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  14. 1 0
      src/PixiEditor/Styles/Templates/NodeGraphView.axaml
  15. 1 1
      src/PixiEditor/Styles/Templates/NodePicker.axaml
  16. 14 8
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  17. 6 5
      src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs
  18. 1 1
      src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs
  19. 5 0
      src/PixiEditor/ViewModels/Nodes/Properties/GenericEnumPropertyViewModel.cs
  20. 13 5
      src/PixiEditor/ViewModels/SubViewModels/IoViewModel.cs
  21. 2 2
      src/PixiEditor/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs
  22. 1 7
      src/PixiEditor/ViewModels/ViewModelMain.cs
  23. 1 1
      src/PixiEditor/Views/Dialogs/AboutPopup.axaml
  24. 1 1
      src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml
  25. 1 1
      src/PixiEditor/Views/Dialogs/NewFilePopup.axaml
  26. 1 0
      src/PixiEditor/Views/Dialogs/PixiEditorPopup.cs
  27. 1 1
      src/PixiEditor/Views/Dialogs/ResizeCanvasPopup.axaml
  28. 1 1
      src/PixiEditor/Views/Dialogs/ResizeDocumentPopup.axaml
  29. 1 1
      src/PixiEditor/Views/Dialogs/ShortcutsPopup.axaml
  30. 6 0
      src/PixiEditor/Views/Nodes/ConnectionView.cs
  31. 106 20
      src/PixiEditor/Views/Nodes/NodeGraphView.cs
  32. 5 0
      src/PixiEditor/Views/Nodes/NodePicker.cs
  33. 6 1
      src/PixiEditor/Views/Nodes/Properties/GenericEnumPropertyView.axaml
  34. 1 1
      src/PixiEditor/Views/Windows/Settings/SettingsWindow.axaml

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit f594ce467f0834a1ce34ab88d892a13a26694d80
+Subproject commit 53075856f14518af3f0b59910a9b9d8339368347

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -98,7 +98,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         }
         else
         {
-            workingSurface.Canvas.DrawSurface(fullResrenderedSurface.DrawingSurface, -(VecI)topLeft, paint);
+            workingSurface.Canvas.DrawSurface(fullResrenderedSurface.DrawingSurface, -topLeft, paint);
         }
 
         workingSurface.Canvas.RestoreToCount(saved);

+ 10 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs

@@ -16,6 +16,9 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
     public override RectD GeometryAABB =>
         new ShapeCorners(Center, Radius * 2).AABBBounds;
 
+    public override RectD VisualAABB =>
+        RectD.FromCenterAndSize(Center, Radius * 2).Inflate(StrokeWidth / 2);
+
     public override ShapeCorners TransformationCorners =>
         new ShapeCorners(Center, Radius * 2).WithMatrix(TransformationMatrix);
 
@@ -51,10 +54,13 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
         shapePaint.Style = PaintStyle.Fill;
         drawingSurface.Canvas.DrawOval(Center, Radius, shapePaint);
 
-        shapePaint.Color = StrokeColor;
-        shapePaint.Style = PaintStyle.Stroke;
-        shapePaint.StrokeWidth = StrokeWidth;
-        drawingSurface.Canvas.DrawOval(Center, Radius, shapePaint);
+        if (StrokeWidth > 0)
+        {
+            shapePaint.Color = StrokeColor;
+            shapePaint.Style = PaintStyle.Stroke;
+            shapePaint.StrokeWidth = StrokeWidth;
+            drawingSurface.Canvas.DrawOval(Center, Radius, shapePaint);
+        }
 
         if (applyTransform)
         {

+ 17 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs

@@ -29,10 +29,26 @@ public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnl
     {
         get
         {
-            return RectD.FromTwoPoints(Start, End).Inflate(StrokeWidth);
+            var dir = (End - Start).Normalize();
+            var cross = new VecD(-dir.Y, dir.X);
+            VecD offset = cross * StrokeWidth / 2;
+
+            VecD topLeft = Start + offset;
+            VecD bottomRight = End - offset;
+            VecD bottomLeft = Start - offset;
+            VecD topRight = End + offset;
+
+            ShapeCorners corners = new ShapeCorners()
+            {
+                TopLeft = topLeft, BottomRight = bottomRight, BottomLeft = bottomLeft, TopRight = topRight
+            };
+
+            return corners.AABBBounds;
         }
     }
 
+    public override RectD VisualAABB => GeometryAABB;
+
     public override ShapeCorners TransformationCorners => new ShapeCorners(GeometryAABB)
         .WithMatrix(TransformationMatrix);
 

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs

@@ -11,7 +11,8 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 public class PathVectorData : ShapeVectorData, IReadOnlyPathData
 {
     public VectorPath Path { get; }
-    public override RectD GeometryAABB => Path.TightBounds.Inflate(StrokeWidth);
+    public override RectD GeometryAABB => Path.TightBounds;
+    public override RectD VisualAABB => GeometryAABB.Inflate(StrokeWidth / 2);
 
     public override ShapeCorners TransformationCorners =>
         new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix);

+ 2 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs

@@ -17,6 +17,8 @@ public class PointsVectorData : ShapeVectorData
     public override RectD GeometryAABB => new RectD(Points.Min(p => p.X), Points.Min(p => p.Y), Points.Max(p => p.X),
         Points.Max(p => p.Y));
 
+    public override RectD VisualAABB => GeometryAABB;
+
     public override ShapeCorners TransformationCorners => new ShapeCorners(
         GeometryAABB).WithMatrix(TransformationMatrix);
 

+ 17 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs

@@ -14,6 +14,16 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
 
     public override RectD GeometryAABB => RectD.FromCenterAndSize(Center, Size);
 
+    public override RectD VisualAABB
+    {
+        get
+        {
+            RectD bounds = RectD.FromCenterAndSize(Center, Size);
+            bounds = bounds.Inflate(StrokeWidth / 2);
+            return bounds;
+        }
+    }
+
     public override ShapeCorners TransformationCorners =>
         new ShapeCorners(Center, Size).WithMatrix(TransformationMatrix);
 
@@ -49,11 +59,14 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         paint.Style = PaintStyle.Fill;
         drawingSurface.Canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
 
-        paint.Color = StrokeColor;
-        paint.Style = PaintStyle.Stroke;
+        if (StrokeWidth > 0)
+        {
+            paint.Color = StrokeColor;
+            paint.Style = PaintStyle.Stroke;
 
-        paint.StrokeWidth = StrokeWidth;
-        drawingSurface.Canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+            paint.StrokeWidth = StrokeWidth;
+            drawingSurface.Canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+        }
 
         if (applyTransform)
         {

+ 3 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs

@@ -15,8 +15,10 @@ public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVe
     public Color StrokeColor { get; set; } = Colors.White;
     public Color FillColor { get; set; } = Colors.White;
     public float StrokeWidth { get; set; } = 1;
-    public abstract RectD GeometryAABB { get; }
+    public abstract RectD GeometryAABB { get; } 
+    public abstract RectD VisualAABB { get; }
     public RectD TransformedAABB => new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix).AABBBounds;
+    public RectD TransformedVisualAABB => new ShapeCorners(VisualAABB).WithMatrix(TransformationMatrix).AABBBounds;
     public abstract ShapeCorners TransformationCorners { get; } 
     
     protected void ApplyTransformTo(DrawingSurface drawingSurface)

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

@@ -79,7 +79,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         }
         else
         {
-            return ShapeData?.TransformedAABB;
+            return ShapeData?.TransformedVisualAABB;
         }
         
         return null;
@@ -95,7 +95,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
 
         using var paint = new Paint();
 
-        VecI tightBoundsSize = (VecI)ShapeData.TransformedAABB.Size;
+        VecI tightBoundsSize = (VecI)ShapeData.TransformedVisualAABB.Size;
 
         VecI translation = new VecI(
             (int)Math.Max(ShapeData.TransformedAABB.TopLeft.X, 0),
@@ -149,7 +149,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
 
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
     {
-        return ShapeData?.TransformedAABB ?? null;
+        return ShapeData?.TransformedVisualAABB ?? null;
     }
 
     public override ShapeCorners GetTransformationCorners(KeyFrameTime frameTime)

+ 58 - 22
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodePosition_UpdateableChange.cs

@@ -6,16 +6,19 @@ namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
 
 internal class NodePosition_UpdateableChange : UpdateableChange
 {
-    public Guid NodeId { get; }
-    public VecD NewPosition { get; private set; } 
-    
-    private VecD originalPosition;
+    public Guid[] NodeIds { get; }
+    public VecD NewPosition { get; private set; }
+
+    private Dictionary<Guid, VecD> originalPositions;
     
+    private VecD startPosition;
+
     [GenerateUpdateableChangeActions]
-    public NodePosition_UpdateableChange(Guid nodeId, VecD newPosition)
+    public NodePosition_UpdateableChange(IEnumerable<Guid> nodeIds, VecD newPosition)
     {
-        NodeId = nodeId;
+        NodeIds = nodeIds.ToArray();
         NewPosition = newPosition;
+        startPosition = newPosition;
     }
 
     [UpdateChangeMethod]
@@ -23,43 +26,76 @@ internal class NodePosition_UpdateableChange : UpdateableChange
     {
         NewPosition = newPosition;
     }
-    
+
     public override bool InitializeAndValidate(Document target)
     {
-        var node = target.FindNode<Node>(NodeId);
-        if (node == null)
+        originalPositions = new Dictionary<Guid, VecD>();
+        foreach (var nodeId in NodeIds)
         {
-            return false;
+            var node = target.FindNode<Node>(nodeId);
+            if (node == null)
+            {
+                return false;
+            }
+            
+            originalPositions.Add(nodeId, node.Position);
         }
 
-        originalPosition = node.Position;
         return true;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
-        var node = target.FindNode<Node>(NodeId);
-        node.Position = NewPosition;
-        return new NodePosition_ChangeInfo(NodeId, NewPosition);
+        List<IChangeInfo> changes = new();
+        VecD delta = NewPosition - startPosition;
+        foreach (var nodeId in NodeIds)
+        {
+            var node = target.FindNode<Node>(nodeId);
+            node.Position = originalPositions[nodeId] + delta;
+            changes.Add(new NodePosition_ChangeInfo(nodeId, node.Position));
+        }
+        
+        return changes;
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         ignoreInUndo = false;
-        var node = target.FindNode<Node>(NodeId);
-        node.Position = NewPosition;
-        return new NodePosition_ChangeInfo(NodeId, NewPosition);
+      
+        VecD delta = NewPosition - startPosition;
+        if (NewPosition == startPosition)
+        {
+            delta = NewPosition;
+        }
+            
+        List<IChangeInfo> changes = new();
+        
+        foreach (var nodeId in NodeIds)
+        {
+            var node = target.FindNode<Node>(nodeId);
+            node.Position = originalPositions[nodeId] + delta;
+            changes.Add(new NodePosition_ChangeInfo(nodeId, node.Position));
+        }
+        
+        return changes;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
-        var node = target.FindNode<Node>(NodeId);
-        node.Position = originalPosition;
-        return new NodePosition_ChangeInfo(NodeId, originalPosition);
+        List<IChangeInfo> changes = new();
+        foreach (var nodeId in NodeIds)
+        {
+            var node = target.FindNode<Node>(nodeId);
+            node.Position = originalPositions[nodeId];
+            changes.Add(new NodePosition_ChangeInfo(nodeId, node.Position));
+        }
+        
+        return changes;
     }
 
     public override bool IsMergeableWith(Change other)
     {
-        return other is NodePosition_UpdateableChange change && change.NodeId == NodeId;
+        return other is NodePosition_UpdateableChange change && change.NodeIds.SequenceEqual(NodeIds);
     }
 }

+ 25 - 13
src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs

@@ -2,6 +2,7 @@
 using PixiEditor.ChangeableDocument.ChangeInfos.Root;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 namespace PixiEditor.ChangeableDocument.Changes.Root;
 
@@ -16,29 +17,34 @@ internal class ClipCanvas_Change : ResizeBasedChangeBase
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
-        RectI? bounds = null;
+        RectD? bounds = null;
         target.ForEveryMember((member) =>
         {
-            if (member is LayerNode layer)
+            if (member.IsVisible.Value)
             {
-                var layerBounds = layer.GetTightBounds(frameToClip);
-                if (layerBounds.HasValue)
+                if (member is LayerNode layer)
                 {
-                    bounds ??= (RectI)layerBounds.Value;
-                    bounds = bounds.Value.Union((RectI)layerBounds.Value);
+                    var layerBounds = layer.GetTightBounds(frameToClip);
+                    if (layerBounds is { IsZeroOrNegativeArea: false })
+                    {
+                        bounds ??= layerBounds.Value;
+                        bounds = bounds.Value.Union(layerBounds.Value);
+                    }
                 }
             }
         });
 
-        if (!bounds.HasValue || bounds.Value.IsZeroOrNegativeArea || bounds.Value == new RectI(VecI.Zero, target.Size))
+        if (!bounds.HasValue || bounds.Value.IsZeroOrNegativeArea || bounds.Value == new RectD(VecI.Zero, target.Size))
         {
             ignoreInUndo = true;
             return new None();
         }
         
-        RectI newBounds = bounds.Value;
+        RectD newBounds = bounds.Value;
         
-        target.Size = newBounds.Size;
+        VecI size = (VecI)newBounds.Size.Ceiling();
+        
+        target.Size = size;
         target.VerticalSymmetryAxisX = Math.Clamp(_originalVerAxisX, 0, target.Size.X);
         target.HorizontalSymmetryAxisY = Math.Clamp(_originalHorAxisY, 0, target.Size.Y);
         
@@ -48,17 +54,23 @@ internal class ClipCanvas_Change : ResizeBasedChangeBase
             {
                 layer.ForEveryFrame(img =>
                 {
-                    Resize(img, layer.Id, newBounds.Size, -newBounds.Pos, deletedChunks);
+                    Resize(img, layer.Id, size, -(VecI)newBounds.Pos, deletedChunks);
                 });
             }
-            
+            else if (member is ITransformableObject transformableObject)
+            {
+                originalTransformations[member.Id] = transformableObject.TransformationMatrix;
+                VecD floor = new VecD(-(float)newBounds.Pos.X, -(float)newBounds.Pos.Y);
+                transformableObject.TransformationMatrix = transformableObject.TransformationMatrix.PostConcat(Matrix3X3.CreateTranslation((float)floor.X, (float)floor.Y));
+            }
+
             if (member.EmbeddedMask is null)
                 return;
             
-            Resize(member.EmbeddedMask, member.Id, newBounds.Size, -newBounds.Pos, deletedMaskChunks);
+            Resize(member.EmbeddedMask, member.Id, size, -(VecI)newBounds.Pos, deletedMaskChunks);
         });
 
         ignoreInUndo = false;
-        return new Size_ChangeInfo(newBounds.Size, target.VerticalSymmetryAxisX, target.HorizontalSymmetryAxisY);
+        return new Size_ChangeInfo(size, target.VerticalSymmetryAxisX, target.HorizontalSymmetryAxisY);
     }
 }

+ 10 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs

@@ -2,6 +2,7 @@
 using PixiEditor.ChangeableDocument.ChangeInfos.Root;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 namespace PixiEditor.ChangeableDocument.Changes.Root;
 
@@ -12,6 +13,8 @@ internal abstract class ResizeBasedChangeBase : Change
     protected double _originalVerAxisX;
     protected Dictionary<Guid, List<CommittedChunkStorage>> deletedChunks = new();
     protected Dictionary<Guid, List<CommittedChunkStorage>> deletedMaskChunks = new();
+    
+    protected Dictionary<Guid, Matrix3X3> originalTransformations = new();
 
     public ResizeBasedChangeBase()
     {
@@ -57,8 +60,13 @@ internal abstract class ResizeBasedChangeBase : Change
                     img.CommitChanges();
                 });
             }
-
-            // TODO: Add support for different Layer types?
+            else if (member is ITransformableObject transformableObject)
+            {
+                if (originalTransformations.TryGetValue(member.Id, out var transformation))
+                {
+                    transformableObject.TransformationMatrix = transformation;
+                }
+            }
 
             if (member.EmbeddedMask is null)
                 return;

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -41,5 +41,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.0.29")]
-[assembly: AssemblyFileVersion("2.0.0.29")]
+[assembly: AssemblyVersion("2.0.0.30")]
+[assembly: AssemblyFileVersion("2.0.0.30")]

+ 1 - 0
src/PixiEditor/Styles/Templates/NodeGraphView.axaml

@@ -17,6 +17,7 @@
                         </Flyout>
                         </Grid.ContextFlyout>
                         <ItemsControl ZIndex="1" ClipToBounds="False"
+                                      Name="PART_Nodes"
                                       ItemsSource="{Binding NodeGraph.AllNodes, RelativeSource={RelativeSource TemplatedParent}}">
                             <ItemsControl.ItemsPanel>
                                 <ItemsPanelTemplate>

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

@@ -8,7 +8,7 @@
     <ControlTheme TargetType="nodes:NodePicker" x:Key="{x:Type nodes:NodePicker}">
         <Setter Property="Template">
             <ControlTemplate>
-                <Grid MinWidth="200" MaxHeight="400">
+                <Grid Height="400" Width="450">
                     <Grid.RowDefinitions>
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="*" />

+ 14 - 8
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -279,8 +279,9 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     {
         var builderInstance = new DocumentViewModelBuilder();
         builder(builderInstance);
-        
-        (string serializerName, string serializerVersion) serializerData = (builderInstance.SerializerName, builderInstance.SerializerVersion);
+
+        (string serializerName, string serializerVersion) serializerData = (builderInstance.SerializerName,
+            builderInstance.SerializerVersion);
 
         Dictionary<int, Guid> mappedNodeIds = new();
         Dictionary<int, Guid> mappedKeyFrameIds = new();
@@ -327,7 +328,10 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         AddAnimationData(builderInstance.AnimationData, mappedNodeIds, mappedKeyFrameIds);
 
         acc.AddFinishedActions(new ChangeBoundary_Action(), new DeleteRecordedChanges_Action());
-        viewModel.MarkAsSaved();
+        acc.AddActions(new InvokeAction_PassthroughAction(() =>
+        {
+            viewModel.MarkAsSaved();
+        }));
 
         return viewModel;
 
@@ -348,7 +352,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 if (serializedNode.AdditionalData != null && serializedNode.AdditionalData.Count > 0)
                 {
                     acc.AddActions(new DeserializeNodeAdditionalData_Action(nodeGuid,
-                        SerializationUtil.DeserializeDict(serializedNode.AdditionalData, config, allFactories, serializerData)));
+                        SerializationUtil.DeserializeDict(serializedNode.AdditionalData, config, allFactories,
+                            serializerData)));
                 }
 
                 if (node.InputConnections != null)
@@ -373,14 +378,15 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             Guid guid = Guid.NewGuid();
             mappedNodeIds.Add(id, guid);
             acc.AddActions(new CreateNodeFromName_Action(serializedNode.UniqueNodeName, guid));
-            acc.AddFinishedActions(new NodePosition_Action(guid, serializedNode.Position.ToVecD()),
+            acc.AddFinishedActions(new NodePosition_Action([guid], serializedNode.Position.ToVecD()),
                 new EndNodePosition_Action());
 
             if (serializedNode.InputValues != null)
             {
                 foreach (var propertyValue in serializedNode.InputValues)
                 {
-                    object value = SerializationUtil.Deserialize(propertyValue.Value, config, allFactories, serializerData);
+                    object value =
+                        SerializationUtil.Deserialize(propertyValue.Value, config, allFactories, serializerData);
                     acc.AddActions(new UpdatePropertyValue_Action(guid, propertyValue.Key, value));
                 }
             }
@@ -531,7 +537,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 using Texture texture = new Texture(renderSize);
                 texture.DrawingSurface.Canvas.Save();
                 VecD scaling = new VecD(renderSize.X / (double)SizeBindable.X, renderSize.Y / (double)SizeBindable.Y);
-                
+
                 texture.DrawingSurface.Canvas.Scale((float)scaling.X, (float)scaling.Y);
                 Renderer.RenderDocument(texture.DrawingSurface, frameTime);
 
@@ -616,7 +622,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 finalBounds = finalBounds.Union(combinedBounds);
             }
         }
-        
+
         if (finalBounds.IsZeroOrNegativeArea)
             return new None();
 

+ 6 - 5
src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs

@@ -209,9 +209,10 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
         return new Queue<INodeHandler>(finalQueue);
     }
 
-    public void SetNodePosition(INodeHandler node, VecD newPos)
+    public void SetNodePositions(List<INodeHandler> node, VecD startPos)
     {
-        Internals.ActionAccumulator.AddActions(new NodePosition_Action(node.Id, newPos));
+        Guid[] nodeIds = node.Select(x => x.Id).ToArray();
+        Internals.ActionAccumulator.AddActions(new NodePosition_Action(nodeIds, startPos));
     }
 
     public void UpdatePropertyValue(INodeHandler node, string property, object? value)
@@ -240,9 +241,9 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
 
             if (pos != default)
             {
-                changes.Add(new NodePosition_Action(startId, pos));
+                changes.Add(new NodePosition_Action([startId], pos));
                 changes.Add(new EndNodePosition_Action());
-                changes.Add(new NodePosition_Action(endId, new VecD(pos.X + 400, pos.Y)));
+                changes.Add(new NodePosition_Action([endId], new VecD(pos.X + 400, pos.Y)));
                 changes.Add(new EndNodePosition_Action());
             }
         }
@@ -253,7 +254,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
 
             if (pos != default)
             {
-                changes.Add(new NodePosition_Action(nodeId, pos));
+                changes.Add(new NodePosition_Action([nodeId], pos));
                 changes.Add(new EndNodePosition_Action());
             }
         }

+ 1 - 1
src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs

@@ -93,7 +93,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             if (!Document.BlockingUpdateableChangeActive)
             {
                 Internals.ActionAccumulator.AddFinishedActions(
-                    new NodePosition_Action(Id, value),
+                    new NodePosition_Action([Id], value),
                     new EndNodePosition_Action());
             }
         }

+ 5 - 0
src/PixiEditor/ViewModels/Nodes/Properties/GenericEnumPropertyViewModel.cs

@@ -10,4 +10,9 @@ internal class GenericEnumPropertyViewModel : NodePropertyViewModel
     }
 
     public Array Values { get; }
+    public int SelectedIndex
+    {
+        get => Value == null ? -1 : Array.IndexOf(Values, Value);
+        set => Value = Values.GetValue(value);
+    }
 }

+ 13 - 5
src/PixiEditor/ViewModels/SubViewModels/IoViewModel.cs

@@ -254,15 +254,19 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
     
     private void HandleRightMouseEraseDown(IToolsHandler tools)
     {
+        EraserToolViewModel? eraserTool = tools.GetTool<EraserToolViewModel>();
+        if (eraserTool == null)
+        {
+            return;
+        }
+        
         var currentToolSize = tools.ActiveTool.Toolbar.Settings.FirstOrDefault(x => x.Name == "ToolSize");
         hadSharedToolbar = tools.EnableSharedToolbar;
         if (currentToolSize != null)
         {
             tools.EnableSharedToolbar = false;
-            var eraserTool = tools.GetTool<EraserToolViewModel>();
-            if(eraserTool == null) return;
             
-            var toolSize = tools.GetTool<EraserToolViewModel>().Toolbar.Settings.First(x => x.Name == "ToolSize");
+            var toolSize = eraserTool.Toolbar.Settings.First(x => x.Name == "ToolSize");
             previousEraseSize = (double)toolSize.Value;
             toolSize.Value = tools.ActiveTool is PenToolViewModel { PixelPerfectEnabled: true }
                 ? 1
@@ -364,8 +368,12 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
         tools.EnableSharedToolbar = hadSharedToolbar;
         if (previousEraseSize != null)
         {
-            tools.GetTool<EraserToolViewModel>().Toolbar.Settings.First(x => x.Name == "ToolSize").Value =
-                previousEraseSize.Value;
+            EraserToolViewModel? eraserTool = tools.GetTool<EraserToolViewModel>();
+            if (eraserTool != null)
+            {
+                eraserTool.Toolbar.Settings.First(x => x.Name == "ToolSize").Value =
+                    previousEraseSize.Value;
+            }
         }
 
         tools.RestorePreviousTool();

+ 2 - 2
src/PixiEditor/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs

@@ -67,9 +67,9 @@ internal class NodeGraphManagerViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Internal("PixiEditor.NodeGraph.ChangeNodePos")]
-    public void ChangeNodePos((INodeHandler node, VecD newPos) args)
+    public void ChangeNodePos((List<INodeHandler> nodes, VecD newPos) args)
     {
-        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.SetNodePosition(args.node, args.newPos);
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.SetNodePositions(args.nodes, args.newPos);
     }
 
     [Command.Internal("PixiEditor.NodeGraph.UpdateValue", AnalyticsTrack = true)]

+ 1 - 7
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -244,6 +244,7 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
         const string ConfirmationDialogTitle = "UNSAVED_CHANGES";
         const string ConfirmationDialogMessage = "DOCUMENT_MODIFIED_SAVE";
 
+
         ConfirmationType result = ConfirmationType.No;
         if (!document.AllChangesSaved)
         {
@@ -268,13 +269,6 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
                     WindowSubViewModel.MakeDocumentViewportActive(null);
             }
 
-            // TODO: this thing should actually dispose the document to free up ram
-            // We need the UI to be able to handle disposed documents
-            // Like, the viewports should show nothing, the commands shouldn't work, etc. At least nothing should crash or behave unexpectedly
-            // Mostly we only care about this because avalondock doesn't remove the UI elements of closed viewports (at least not right away)
-            // So they remain alive and keep "showing" the now disposed DocumentViewModel
-            // And since they reference the DocumentViewModel it doesn't get collected by GC
-
             WindowSubViewModel.CloseViewportsForDocument(document);
             document.Dispose();
 

+ 1 - 1
src/PixiEditor/Views/Dialogs/AboutPopup.axaml

@@ -13,7 +13,7 @@
         CanResize="True"
         Width="440" Height="540"
         MaxWidth="440" MaxHeight="540"
-        Title="ABOUT">
+        ui:Translator.Key="ABOUT">
     <StackPanel DataContext="{Binding ElementName=aboutPopup}" Orientation="Vertical" DockPanel.Dock="Bottom" Margin="10">
             <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Top">
                 <Svg Path="../../Images/PixiEditorLogo.svg" Height="40" VerticalAlignment="Center"/>

+ 1 - 1
src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml

@@ -11,7 +11,7 @@
                          Name="saveFilePopup"
                          x:Class="PixiEditor.Views.Dialogs.ExportFilePopup"
                          x:ClassModifier="internal"
-                         Title="EXPORT_IMAGE">
+                         ui1:Translator.Key="EXPORT_IMAGE">
     <DockPanel Background="{DynamicResource ThemeBackgroundBrush}">
         <Button DockPanel.Dock="Bottom" HorizontalAlignment="Center" IsDefault="True"
                 ui1:Translator.Key="EXPORT" Command="{Binding ExportCommand, ElementName=saveFilePopup}" />

+ 1 - 1
src/PixiEditor/Views/Dialogs/NewFilePopup.axaml

@@ -12,7 +12,7 @@
         CanResize="False"
         CanMinimize="False"
         Name="popup"
-        Title="CREATE_NEW_IMAGE">
+        ui:Translator.Key="CREATE_NEW_IMAGE">
     <StackPanel Background="{DynamicResource ThemeBackgroundBrush}" Focusable="True" Orientation="Vertical">
         <input:SizePicker HorizontalAlignment="Center" MinWidth="230" Height="125" Margin="15,30,15,0"
                           PreserveAspectRatio="False"

+ 1 - 0
src/PixiEditor/Views/Dialogs/PixiEditorPopup.cs

@@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.Input;
 using PixiEditor.Extensions.CommonApi;
 using PixiEditor.Extensions.CommonApi.Async;
 using PixiEditor.Extensions.CommonApi.Windowing;
+using PixiEditor.Extensions.UI;
 
 namespace PixiEditor.Views.Dialogs;
 

+ 1 - 1
src/PixiEditor/Views/Dialogs/ResizeCanvasPopup.axaml

@@ -11,7 +11,7 @@
                          x:Name="window"
                          CanResize="False"
                          CanMinimize="False"
-                         Title="RESIZE_CANVAS"
+                         ui:Translator.Key="RESIZE_CANVAS"
                          Height="410" Width="320">
     <DockPanel Focusable="True">
         <Button DockPanel.Dock="Bottom" HorizontalAlignment="Center" Margin="15"

+ 1 - 1
src/PixiEditor/Views/Dialogs/ResizeDocumentPopup.axaml

@@ -8,7 +8,7 @@
                              xmlns:input="clr-namespace:PixiEditor.Views.Input"
                              xmlns:dialogs="clr-namespace:PixiEditor.Views.Dialogs"
                              mc:Ignorable="d" Name="window"
-                             Title="RESIZE_IMAGE"
+                             ui:Translator.Key="RESIZE_IMAGE"
                              CanResize="False"
                              CanMinimize="False"
                              Height="305" Width="310" MinHeight="305" MinWidth="310">

+ 1 - 1
src/PixiEditor/Views/Dialogs/ShortcutsPopup.axaml

@@ -16,7 +16,7 @@
                          x:Class="PixiEditor.Views.Dialogs.ShortcutsPopup"
                          CloseIsHide="True"
                          x:ClassModifier="internal"
-                         Title="SHORTCUTS_TITLE">
+                         ui:Translator.Key="SHORTCUTS_TITLE">
     <Grid>
         <Grid.Styles>
             <Style Selector="ItemsControl">

+ 6 - 0
src/PixiEditor/Views/Nodes/ConnectionView.cs

@@ -87,6 +87,12 @@ internal class ConnectionView : TemplatedControl
             EndPoint = CalculateSocketPoint(OutputProperty);
         }, DispatcherPriority.Render);
     }
+    
+    public void UpdateSocketPoints()
+    {
+        StartPoint = CalculateSocketPoint(InputProperty);
+        EndPoint = CalculateSocketPoint(OutputProperty);
+    }
 
     private Point CalculateSocketPoint(BindingValue<NodePropertyViewModel> argsNewValue)
     {

+ 106 - 20
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -7,6 +7,7 @@ using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Media;
+using Avalonia.Threading;
 using Avalonia.VisualTree;
 using CommunityToolkit.Mvvm.Input;
 using PixiEditor.Helpers;
@@ -74,8 +75,9 @@ internal class NodeGraphView : Zoombox.Zoombox
         AvaloniaProperty.Register<NodeGraphView, ICommand>(
             "ConnectPropertiesCommand");
 
-    public static readonly StyledProperty<ICommand> CreateNodeFromContextCommandProperty = AvaloniaProperty.Register<NodeGraphView, ICommand>(
-        "CreateNodeFromContextCommand");
+    public static readonly StyledProperty<ICommand> CreateNodeFromContextCommandProperty =
+        AvaloniaProperty.Register<NodeGraphView, ICommand>(
+            "CreateNodeFromContextCommand");
 
     public ICommand CreateNodeFromContextCommand
     {
@@ -94,7 +96,7 @@ internal class NodeGraphView : Zoombox.Zoombox
         get => GetValue(AllNodeTypesProperty);
         set => SetValue(AllNodeTypesProperty, value);
     }
-    
+
     public ObservableCollection<NodeTypeInfo> AllNodeTypeInfos
     {
         get => GetValue(AllNodeTypeInfosProperty);
@@ -188,7 +190,12 @@ internal class NodeGraphView : Zoombox.Zoombox
     private NodeConnectionViewModel? _hiddenConnection;
     private Color _startingPropColor;
     private VecD _lastMouseClickPos;
-    public static readonly StyledProperty<int> ActiveFrameProperty = AvaloniaProperty.Register<NodeGraphView, int>("ActiveFrame");
+
+    private ItemsControl nodeItemsControl;
+    private ItemsControl connectionItemsControl;
+
+    public static readonly StyledProperty<int> ActiveFrameProperty =
+        AvaloniaProperty.Register<NodeGraphView, int>("ActiveFrame");
 
     public NodeGraphView()
     {
@@ -202,7 +209,53 @@ internal class NodeGraphView : Zoombox.Zoombox
         AllNodeTypes = new ObservableCollection<Type>(GatherAssemblyTypes<NodeViewModel>());
         AllNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(AllNodeTypes.Select(x => new NodeTypeInfo(x)));
     }
-    
+
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+        nodeItemsControl = e.NameScope.Find<ItemsControl>("PART_Nodes");
+        connectionItemsControl = e.NameScope.Find<ItemsControl>("PART_Connections");
+
+        Dispatcher.UIThread.Post(() =>
+        {
+            nodeItemsControl.ItemsPanelRoot.Children.CollectionChanged += Items_CollectionChanged;
+        });
+    }
+
+    private void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+    {
+        if (e.Action == NotifyCollectionChangedAction.Add)
+        {
+            foreach (Control control in e.NewItems)
+            {
+                if (control is not ContentPresenter presenter)
+                {
+                    continue;
+                }
+
+                if (presenter.Child == null)
+                {
+                    presenter.PropertyChanged += OnPresenterPropertyChanged;
+                    continue;
+                }
+
+                NodeView nodeView = (NodeView)presenter.Child;
+                nodeView.PropertyChanged += NodeView_PropertyChanged;
+            }
+        }
+    }
+
+    private void OnPresenterPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.Property == ContentPresenter.ChildProperty)
+        {
+            if (e.NewValue is NodeView nodeView)
+            {
+                nodeView.PropertyChanged += NodeView_PropertyChanged;
+            }
+        }
+    }
+
     private void CreateNodeType(NodeTypeInfo nodeType)
     {
         var type = nodeType.NodeType;
@@ -221,7 +274,7 @@ internal class NodeGraphView : Zoombox.Zoombox
         {
             ClearSelection();
         }
-        
+
         Point pos = e.GetPosition(this);
         _lastMouseClickPos = ToZoomboxSpace(new VecD(pos.X, pos.Y));
     }
@@ -240,7 +293,7 @@ internal class NodeGraphView : Zoombox.Zoombox
         VecD currentPoint = ToZoomboxSpace(new VecD(pos.X, pos.Y));
 
         NodeSocket? nodeSocket = e.Source as NodeSocket;
-        
+
         if (nodeSocket != null)
         {
             Canvas canvas = nodeSocket.FindAncestorOfType<Canvas>();
@@ -276,8 +329,7 @@ internal class NodeGraphView : Zoombox.Zoombox
             {
                 GradientStops = new GradientStops()
                 {
-                    new GradientStop(gradientStopFirstColor, 0),
-                    new GradientStop(gradientStopSecondColor, 1),
+                    new GradientStop(gradientStopFirstColor, 0), new GradientStop(gradientStopSecondColor, 1),
                 }
             };
         }
@@ -299,8 +351,8 @@ internal class NodeGraphView : Zoombox.Zoombox
         {
             return gradientBrush.GradientStops.FirstOrDefault()?.Color;
         }
-        
-        return null; 
+
+        return null;
     }
 
     protected override void OnPointerReleased(PointerReleasedEventArgs e)
@@ -321,6 +373,11 @@ internal class NodeGraphView : Zoombox.Zoombox
             isDraggingConnection = false;
             _hiddenConnection = null;
         }
+
+        if (e.Source is NodeView nodeView)
+        {
+            UpdateConnections(nodeView);
+        }
     }
 
     private IEnumerable<Type> GatherAssemblyTypes<T>()
@@ -363,6 +420,15 @@ internal class NodeGraphView : Zoombox.Zoombox
         }
     }
 
+    private void NodeView_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.Property == BoundsProperty)
+        {
+            NodeView nodeView = (NodeView)sender!;
+            UpdateConnections(nodeView);
+        }
+    }
+
     private NodeView FindNodeView(INodeHandler node)
     {
         return this.GetVisualDescendants().OfType<NodeView>().FirstOrDefault(x => x.Node == node);
@@ -380,13 +446,10 @@ internal class NodeGraphView : Zoombox.Zoombox
         }
 
         _previewConnectionLine.IsVisible = true;
-        _startingPropColor = GetSocketColor(nodeSocket) ?? Colors.White; 
+        _startingPropColor = GetSocketColor(nodeSocket) ?? Colors.White;
         _previewConnectionLine.LineBrush = new LinearGradientBrush()
         {
-            GradientStops = new GradientStops()
-            {
-                new GradientStop(_startingPropColor, 1),
-            }
+            GradientStops = new GradientStops() { new GradientStop(_startingPropColor, 1), }
         };
 
         _previewConnectionLine.StartPoint = nodeSocket.ConnectPort.TranslatePoint(
@@ -396,6 +459,32 @@ internal class NodeGraphView : Zoombox.Zoombox
         startDragConnectionPoint = _previewConnectionLine.StartPoint;
     }
 
+    private void UpdateConnections(NodeView nodeView)
+    {
+        foreach (NodePropertyView propertyView in nodeView.GetVisualDescendants().OfType<NodePropertyView>())
+        {
+            NodePropertyViewModel property = (NodePropertyViewModel)propertyView.DataContext;
+            UpdateConnectionView(property);
+        }
+    }
+
+    private void UpdateConnectionView(NodePropertyViewModel? propertyView)
+    {
+        foreach (var connection in connectionItemsControl.ItemsPanelRoot.Children)
+        {
+            if (connection is ContentPresenter contentPresenter)
+            {
+                ConnectionView connectionView = (ConnectionView)contentPresenter.FindDescendantOfType<ConnectionView>();
+
+                if (connectionView.InputProperty == propertyView || connectionView.OutputProperty == propertyView)
+                {
+                    connectionView.UpdateSocketPoints();
+                    connectionView.InvalidateVisual();
+                }
+            }
+        }
+    }
+
     private void Dragged(PointerEventArgs e)
     {
         if (isDraggingNodes)
@@ -404,10 +493,7 @@ internal class NodeGraphView : Zoombox.Zoombox
             VecD currentPoint = ToZoomboxSpace(new VecD(pos.X, pos.Y));
 
             VecD delta = currentPoint - clickPointOffset;
-            foreach (var node in SelectedNodes)
-            {
-                ChangeNodePosCommand?.Execute((node, initialNodePositions[SelectedNodes.IndexOf(node)] + delta));
-            }
+            ChangeNodePosCommand?.Execute((SelectedNodes, initialNodePositions[0] + delta));
         }
     }
 

+ 5 - 0
src/PixiEditor/Views/Nodes/NodePicker.cs

@@ -241,6 +241,11 @@ public partial class NodePicker : TemplatedControl
         }
 
         var nodes = NodeAbbreviation.FromString(SearchQuery, AllNodeTypeInfos);
+        
+        if(nodes == null || nodes.Count == 0)
+        {
+            return;
+        }
 
         if (nodes == null && FilteredNodeGroups.Count > 0)
         {

+ 6 - 1
src/PixiEditor/Views/Nodes/Properties/GenericEnumPropertyView.axaml

@@ -6,11 +6,16 @@
                              xmlns:input="clr-namespace:PixiEditor.Views.Input"
                              xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
                              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.GenericEnumPropertyView">
+    <Design.DataContext>
+        <properties1:GenericEnumPropertyViewModel />
+    </Design.DataContext>
+    
     <Grid HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
         <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
         <ComboBox HorizontalAlignment="Right" MinWidth="100" IsVisible="{Binding ShowInputField}"
-                  SelectedValue="{Binding Value, Mode=TwoWay}" ItemsSource="{Binding Values}" />
+                  SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" ItemsSource="{Binding Values}" />
     </Grid>
 </properties:NodePropertyView>

+ 1 - 1
src/PixiEditor/Views/Windows/Settings/SettingsWindow.axaml

@@ -24,7 +24,7 @@
     MinWidth="665" MinHeight="500"
     DataContext="{DynamicResource SettingsWindowViewModel}"
     BorderBrush="Black" BorderThickness="1"
-    Title="SETTINGS">
+    ui:Translator.Key="SETTINGS">
 
     <Window.Resources>
         <vm:SettingsWindowViewModel x:Key="SettingsWindowViewModel"/>