Browse Source

Make ChangeableDocument lenient to invalid actions

Equbuxu 3 years ago
parent
commit
f020596d57
27 changed files with 234 additions and 181 deletions
  1. 6 0
      src/PixiEditor.ChangeableDocument.Gen/Helpers.cs
  2. 1 0
      src/PixiEditor.ChangeableDocument/Actions/IStartOrUpdateChangeAction.cs
  3. 12 2
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  4. 4 2
      src/PixiEditor.ChangeableDocument/Changes/Change.cs
  5. 6 17
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelection_Change.cs
  6. 8 8
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  7. 11 11
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRectangle_UpdateableChange.cs
  8. 13 6
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs
  9. 11 10
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PasteImage_UpdateableChange.cs
  10. 3 7
      src/PixiEditor.ChangeableDocument/Changes/Drawing/SelectRectangle_UpdateableChange.cs
  11. 8 5
      src/PixiEditor.ChangeableDocument/Changes/Properties/CreateStructureMemberMask_Change.cs
  12. 6 8
      src/PixiEditor.ChangeableDocument/Changes/Properties/DeleteStructureMemberMask_Change.cs
  13. 10 6
      src/PixiEditor.ChangeableDocument/Changes/Properties/LayerLockTransparency_Change.cs
  14. 6 5
      src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberBlendMode_Change.cs
  15. 6 5
      src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberClipToMemberBelow_Change.cs
  16. 6 8
      src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberIsVisible_Change.cs
  17. 6 10
      src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberName_Change.cs
  18. 6 6
      src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberOpacity_UpdateableChange.cs
  19. 5 6
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs
  20. 3 4
      src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryAxisPosition_UpdateableChange.cs
  21. 5 11
      src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryAxisState_Change.cs
  22. 7 4
      src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs
  23. 7 6
      src/PixiEditor.ChangeableDocument/Changes/Structure/DeleteStructureMember_Change.cs
  24. 7 5
      src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs
  25. 63 27
      src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs
  26. 7 1
      src/PixiEditor.ChangeableDocument/GlobalUsings.cs
  27. 1 1
      src/README.md

+ 6 - 0
src/PixiEditor.ChangeableDocument.Gen/Helpers.cs

@@ -60,6 +60,12 @@ internal static class Helpers
         AppendProperties(sb, properties);
         AppendCreateUpdateableCorrespondingChange(sb, changeConstructorInfo.ContainingClass, properties);
         AppendUpdateCorrespondingChange(sb, updateMethodInfo.Name, changeConstructorInfo.ContainingClass, updatePropsToPass);
+        sb.AppendLine($@"
+bool PixiEditor.ChangeableDocument.Actions.IStartOrUpdateChangeAction.IsChangeTypeMatching(PixiEditor.ChangeableDocument.Changes.Change change)
+{{
+    return change is {changeConstructorInfo.ContainingClass.NameWithNamespace};
+}}
+");
         sb.AppendLine("}");
 
         return sb.ToString();

+ 1 - 0
src/PixiEditor.ChangeableDocument/Actions/IStartOrUpdateChangeAction.cs

@@ -4,6 +4,7 @@ namespace PixiEditor.ChangeableDocument.Actions;
 
 internal interface IStartOrUpdateChangeAction : IAction
 {
+    bool IsChangeTypeMatching(Change change);
     void UpdateCorrespodingChange(UpdateableChange change);
     UpdateableChange CreateCorrespondingChange();
 }

+ 12 - 2
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -1,5 +1,4 @@
-using ChunkyImageLib.DataHolders;
-using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
 namespace PixiEditor.ChangeableDocument.Changeables;
 
@@ -42,6 +41,17 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
         return (path[0], (Folder)path[1]);
     }
 
+    public (StructureMember?, Folder?) FindChildAndParent(Guid childGuid)
+    {
+        var path = FindMemberPath(childGuid);
+        return path.Count switch
+        {
+            1 => (path[0], null),
+            > 1 => (path[0], (Folder)path[1]),
+            _ => (null, null),
+        };
+    }
+
     public List<StructureMember> FindMemberPath(Guid guid)
     {
         var list = new List<StructureMember>();

+ 4 - 2
src/PixiEditor.ChangeableDocument/Changes/Change.cs

@@ -1,4 +1,6 @@
-using PixiEditor.ChangeableDocument.Changeables;
+using OneOf;
+using OneOf.Types;
+using PixiEditor.ChangeableDocument.Changeables;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 
 namespace PixiEditor.ChangeableDocument.Changes;
@@ -6,7 +8,7 @@ namespace PixiEditor.ChangeableDocument.Changes;
 internal abstract class Change : IDisposable
 {
     public virtual bool IsMergeableWith(Change other) => false;
-    public virtual void Initialize(Document target) { }
+    public abstract OneOf<Success, Error> InitializeAndValidate(Document target);
     public abstract IChangeInfo? Apply(Document target, out bool ignoreInUndo);
     public abstract IChangeInfo? Revert(Document target);
     public virtual void Dispose() { }

+ 6 - 17
src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelection_Change.cs

@@ -1,36 +1,27 @@
-using ChunkyImageLib;
-using ChunkyImageLib.DataHolders;
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
+using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
 using SkiaSharp;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 internal class ClearSelection_Change : Change
 {
-    private bool originalIsEmpty;
     private CommittedChunkStorage? savedSelection;
     private SKPath? originalPath;
 
     [GenerateMakeChangeAction]
     public ClearSelection_Change() { }
 
-    public override void Initialize(Document target)
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
-        originalIsEmpty = target.Selection.IsEmptyAndInactive;
-        if (!originalIsEmpty)
-            savedSelection = new(target.Selection.SelectionImage, target.Selection.SelectionImage.FindAllChunks());
+        if (target.Selection.IsEmptyAndInactive)
+            return new Error();
+        savedSelection = new(target.Selection.SelectionImage, target.Selection.SelectionImage.FindAllChunks());
         originalPath = new SKPath(target.Selection.SelectionPath);
+        return new Success();
     }
 
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
     {
-        if (originalIsEmpty)
-        {
-            ignoreInUndo = true;
-            return null;
-        }
         target.Selection.IsEmptyAndInactive = true;
 
         target.Selection.SelectionImage.CancelChanges();
@@ -47,8 +38,6 @@ internal class ClearSelection_Change : Change
 
     public override IChangeInfo? Revert(Document target)
     {
-        if (originalIsEmpty)
-            return new Selection_ChangeInfo() { Chunks = new() };
         target.Selection.IsEmptyAndInactive = false;
 
         target.Selection.SelectionImage.CancelChanges();

+ 8 - 8
src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs

@@ -1,9 +1,4 @@
-using ChunkyImageLib;
-using ChunkyImageLib.DataHolders;
-using OneOf;
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
+using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
 using PixiEditor.ChangeableDocument.Rendering;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
@@ -24,16 +19,21 @@ internal class CombineStructureMembersOnto_Change : Change
         this.targetLayer = targetLayer;
     }
 
-    public override void Initialize(Document target)
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
+        if (target.FindMember(targetLayer) is null || membersToMerge.Count == 0)
+            return new Error();
         foreach (Guid guid in membersToMerge)
         {
-            var member = target.FindMemberOrThrow(guid);
+            var member = target.FindMember(guid);
+            if (member is null)
+                return new Error();
             if (member is Layer layer)
                 layersToCombine.Add(layer.GuidValue);
             else if (member is Folder innerFolder)
                 AddChildren(innerFolder, layersToCombine);
         }
+        return new Success();
     }
 
     private void AddChildren(Folder folder, HashSet<Guid> collection)

+ 11 - 11
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawRectangle_UpdateableChange.cs

@@ -1,10 +1,4 @@
-using ChunkyImageLib;
-using ChunkyImageLib.DataHolders;
-using PixiEditor.ChangeableDocument.Actions;
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-
-namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 internal class DrawRectangle_UpdateableChange : UpdateableChange
 {
@@ -21,6 +15,13 @@ internal class DrawRectangle_UpdateableChange : UpdateableChange
         this.drawOnMask = drawOnMask;
     }
 
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
+    {
+        if (!DrawingChangeHelper.IsValidForDrawing(target, memberGuid, drawOnMask))
+            return new Error();
+        return new Success();
+    }
+
     [UpdateChangeMethod]
     public void Update(ShapeData rectangle)
     {
@@ -30,7 +31,6 @@ internal class DrawRectangle_UpdateableChange : UpdateableChange
     private HashSet<VecI> UpdateRectangle(Document target, ChunkyImage targetImage)
     {
         var oldAffectedChunks = targetImage.FindAffectedChunks();
-        var targetMember = target.FindMemberOrThrow(memberGuid);
 
         targetImage.CancelChanges();
         DrawingChangeHelper.ApplyClipsSymmetriesEtc(target, targetImage, memberGuid, drawOnMask);
@@ -44,14 +44,14 @@ internal class DrawRectangle_UpdateableChange : UpdateableChange
 
     public override IChangeInfo? ApplyTemporarily(Document target)
     {
-        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImage(target, memberGuid, drawOnMask);
+        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         var chunks = UpdateRectangle(target, targetImage);
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, chunks, drawOnMask);
     }
 
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
     {
-        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImage(target, memberGuid, drawOnMask);
+        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         var affectedChunks = UpdateRectangle(target, targetImage);
         storedChunks = new CommittedChunkStorage(targetImage, affectedChunks!);
         targetImage.CommitChanges();
@@ -62,7 +62,7 @@ internal class DrawRectangle_UpdateableChange : UpdateableChange
 
     public override IChangeInfo? Revert(Document target)
     {
-        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImage(target, memberGuid, drawOnMask);
+        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         storedChunks!.ApplyChunksToImage(targetImage);
         storedChunks.Dispose();
         storedChunks = null;

+ 13 - 6
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs

@@ -1,13 +1,9 @@
-using ChunkyImageLib;
-using ChunkyImageLib.DataHolders;
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
+using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 internal static class DrawingChangeHelper
 {
-    public static ChunkyImage GetTargetImage(Document target, Guid memberGuid, bool drawOnMask)
+    public static ChunkyImage GetTargetImageOrThrow(Document target, Guid memberGuid, bool drawOnMask)
     {
         var member = target.FindMemberOrThrow(memberGuid);
         if (drawOnMask)
@@ -38,6 +34,17 @@ internal static class DrawingChangeHelper
             targetImage.SetVerticalAxisOfSymmetry(target.VerticalSymmetryAxisX);
     }
 
+    public static bool IsValidForDrawing(Document target, Guid memberGuid, bool drawOnMask)
+    {
+        var member = target.FindMember(memberGuid);
+        if (member is null)
+            return false;
+        if (drawOnMask && member.Mask is null)
+            return false;
+        if (!drawOnMask && member is Folder)
+            return false;
+        return true;
+    }
 
     public static IChangeInfo CreateChunkChangeInfo(Guid memberGuid, HashSet<VecI> affectedChunks, bool drawOnMask)
     {

+ 11 - 10
src/PixiEditor.ChangeableDocument/Changes/Drawing/PasteImage_UpdateableChange.cs

@@ -1,10 +1,4 @@
-using ChunkyImageLib;
-using ChunkyImageLib.DataHolders;
-using PixiEditor.ChangeableDocument.Actions;
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-
-namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 internal class PasteImage_UpdateableChange : UpdateableChange
 {
     private ShapeCorners corners;
@@ -24,6 +18,13 @@ internal class PasteImage_UpdateableChange : UpdateableChange
         this.imageToPaste = new Surface(image);
     }
 
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
+    {
+        if (!DrawingChangeHelper.IsValidForDrawing(target, memberGuid, drawOnMask))
+            return new Error();
+        return new Success();
+    }
+
     [UpdateChangeMethod]
     public void Update(ShapeCorners corners)
     {
@@ -46,7 +47,7 @@ internal class PasteImage_UpdateableChange : UpdateableChange
 
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
     {
-        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImage(target, memberGuid, drawOnMask);
+        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         var chunks = DrawImage(target, targetImage);
         savedChunks?.Dispose();
         savedChunks = new(targetImage, targetImage.FindAffectedChunks());
@@ -58,7 +59,7 @@ internal class PasteImage_UpdateableChange : UpdateableChange
 
     public override IChangeInfo? ApplyTemporarily(Document target)
     {
-        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImage(target, memberGuid, drawOnMask);
+        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         return DrawingChangeHelper.CreateChunkChangeInfo(memberGuid, DrawImage(target, targetImage), drawOnMask);
     }
 
@@ -66,7 +67,7 @@ internal class PasteImage_UpdateableChange : UpdateableChange
     {
         if (savedChunks is null)
             throw new InvalidOperationException("No saved chunks to restore");
-        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImage(target, memberGuid, drawOnMask);
+        ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
         savedChunks.ApplyChunksToImage(targetImage);
         var chunks = targetImage.FindAffectedChunks();
         targetImage.CommitChanges();

+ 3 - 7
src/PixiEditor.ChangeableDocument/Changes/Drawing/SelectRectangle_UpdateableChange.cs

@@ -1,9 +1,4 @@
-using ChunkyImageLib;
-using ChunkyImageLib.DataHolders;
-using PixiEditor.ChangeableDocument.Actions;
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
+using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
 using SkiaSharp;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
@@ -21,10 +16,11 @@ internal class SelectRectangle_UpdateableChange : UpdateableChange
     {
         Update(pos, size);
     }
-    public override void Initialize(Document target)
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
         originalIsEmpty = target.Selection.IsEmptyAndInactive;
         originalPath = new SKPath(target.Selection.SelectionPath);
+        return new Success();
     }
 
     [UpdateChangeMethod]

+ 8 - 5
src/PixiEditor.ChangeableDocument/Changes/Properties/CreateStructureMemberMask_Change.cs

@@ -1,7 +1,4 @@
-using ChunkyImageLib;
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 
 namespace PixiEditor.ChangeableDocument.Changes.Properties;
 
@@ -15,7 +12,13 @@ internal class CreateStructureMemberMask_Change : Change
         targetMember = memberGuid;
     }
 
-    public override void Initialize(Document target) { }
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
+    {
+        var member = target.FindMember(targetMember);
+        if (member is null || member.Mask is not null)
+            return new Error();
+        return new Success();
+    }
 
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
     {

+ 6 - 8
src/PixiEditor.ChangeableDocument/Changes/Properties/DeleteStructureMemberMask_Change.cs

@@ -1,7 +1,4 @@
-using ChunkyImageLib;
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 
 namespace PixiEditor.ChangeableDocument.Changes.Properties;
 
@@ -16,12 +13,13 @@ internal class DeleteStructureMemberMask_Change : Change
         this.memberGuid = memberGuid;
     }
 
-    public override void Initialize(Document target)
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
-        var member = target.FindMemberOrThrow(memberGuid);
-        if (member.Mask is null)
-            throw new InvalidOperationException("Cannot delete the mask; Target member has no mask");
+        var member = target.FindMember(memberGuid);
+        if (member is null || member.Mask is null)
+            return new Error();
         storedMask = member.Mask.CloneFromCommitted();
+        return new Success();
     }
 
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)

+ 10 - 6
src/PixiEditor.ChangeableDocument/Changes/Properties/LayerLockTransparency_Change.cs

@@ -1,6 +1,4 @@
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 
 namespace PixiEditor.ChangeableDocument.Changes.Properties;
 internal class LayerLockTransparency_Change : Change
@@ -16,15 +14,21 @@ internal class LayerLockTransparency_Change : Change
         this.newValue = newValue;
     }
 
-    public override void Initialize(Document target)
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
-        originalValue = ((Layer)target.FindMemberOrThrow(layerGuid)).LockTransparency;
+        var member = target.FindMember(layerGuid);
+        if (member is not Layer layer)
+            return new Error();
+        originalValue = layer.LockTransparency;
+        if (originalValue == newValue)
+            return new Error();
+        return new Success();
     }
 
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
     {
         ((Layer)target.FindMemberOrThrow(layerGuid)).LockTransparency = newValue;
-        ignoreInUndo = originalValue == newValue;
+        ignoreInUndo = false;
         return new LayerLockTransparency_ChangeInfo() { GuidValue = layerGuid };
     }
 

+ 6 - 5
src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberBlendMode_Change.cs

@@ -1,6 +1,4 @@
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 using PixiEditor.ChangeableDocument.Enums;
 
 namespace PixiEditor.ChangeableDocument.Changes.Properties;
@@ -17,10 +15,13 @@ internal class StructureMemberBlendMode_Change : Change
         this.targetGuid = memberGuid;
     }
 
-    public override void Initialize(Document target)
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
-        var member = target.FindMemberOrThrow(targetGuid);
+        var member = target.FindMember(targetGuid);
+        if (member is null || member.BlendMode == newBlendMode)
+            return new Error();
         originalBlendMode = member.BlendMode;
+        return new Success();
     }
 
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)

+ 6 - 5
src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberClipToMemberBelow_Change.cs

@@ -1,6 +1,4 @@
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 
 namespace PixiEditor.ChangeableDocument.Changes.Properties;
 internal class StructureMemberClipToMemberBelow_Change : Change
@@ -16,10 +14,13 @@ internal class StructureMemberClipToMemberBelow_Change : Change
         this.memberGuid = memberGuid;
     }
 
-    public override void Initialize(Document target)
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
-        var member = target.FindMemberOrThrow(memberGuid);
+        var member = target.FindMember(memberGuid);
+        if (member is null || member.ClipToMemberBelow == newValue)
+            return new Error();
         originalValue = member.ClipToMemberBelow;
+        return new Success();
     }
 
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)

+ 6 - 8
src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberIsVisible_Change.cs

@@ -1,6 +1,4 @@
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 
 namespace PixiEditor.ChangeableDocument.Changes.Properties;
 
@@ -16,20 +14,20 @@ internal class StructureMemberIsVisible_Change : Change
         this.newIsVisible = isVisible;
     }
 
-    public override void Initialize(Document target)
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
-        var member = target.FindMemberOrThrow(targetMember);
+        var member = target.FindMember(targetMember);
+        if (member is null || member.IsVisible == newIsVisible)
+            return new Error();
         originalIsVisible = member.IsVisible;
+        return new Success();
     }
 
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
     {
         // don't record layer/folder visibility changes - it's just more convenient this way
         ignoreInUndo = true;
-        if (originalIsVisible == newIsVisible)
-            return null;
         target.FindMemberOrThrow(targetMember).IsVisible = newIsVisible;
-
         return new StructureMemberIsVisible_ChangeInfo() { GuidValue = targetMember };
     }
 

+ 6 - 10
src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberName_Change.cs

@@ -1,6 +1,4 @@
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 
 namespace PixiEditor.ChangeableDocument.Changes.Properties;
 
@@ -17,19 +15,17 @@ internal class StructureMemberName_Change : Change
         this.newName = name;
     }
 
-    public override void Initialize(Document target)
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
-        var member = target.FindMemberOrThrow(targetMember);
+        var member = target.FindMember(targetMember);
+        if (member is null || member.Name == newName)
+            return new Error();
         originalName = member.Name;
+        return new Success();
     }
 
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
     {
-        if (originalName == newName)
-        {
-            ignoreInUndo = true;
-            return null;
-        }
         target.FindMemberOrThrow(targetMember).Name = newName;
 
         ignoreInUndo = false;

+ 6 - 6
src/PixiEditor.ChangeableDocument/Changes/Properties/StructureMemberOpacity_UpdateableChange.cs

@@ -1,7 +1,4 @@
-using PixiEditor.ChangeableDocument.Actions;
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
+using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 
 namespace PixiEditor.ChangeableDocument.Changes.Properties;
 
@@ -25,10 +22,13 @@ internal class StructureMemberOpacity_UpdateableChange : UpdateableChange
         newOpacity = opacity;
     }
 
-    public override void Initialize(Document document)
+    public override OneOf<Success, Error> InitializeAndValidate(Document document)
     {
-        var member = document.FindMemberOrThrow(memberGuid);
+        var member = document.FindMember(memberGuid);
+        if (member is null)
+            return new Error();
         originalOpacity = member.Opacity;
+        return new Success();
     }
 
     public override IChangeInfo? ApplyTemporarily(Document target) => Apply(target, out _);

+ 5 - 6
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs

@@ -1,8 +1,4 @@
-using ChunkyImageLib;
-using ChunkyImageLib.DataHolders;
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Root;
+using PixiEditor.ChangeableDocument.ChangeInfos.Root;
 
 namespace PixiEditor.ChangeableDocument.Changes.Root;
 
@@ -21,11 +17,14 @@ internal class ResizeCanvas_Change : Change
     {
         newSize = size;
     }
-    public override void Initialize(Document target)
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
+        if (target.Size == newSize)
+            return new Error();
         originalSize = target.Size;
         originalHorAxisY = target.HorizontalSymmetryAxisY;
         originalVerAxisX = target.VerticalSymmetryAxisX;
+        return new Success();
     }
 
     private void ForEachLayer(Folder folder, Action<Layer> action)

+ 3 - 4
src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryAxisPosition_UpdateableChange.cs

@@ -1,6 +1,4 @@
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Root;
+using PixiEditor.ChangeableDocument.ChangeInfos.Root;
 using PixiEditor.ChangeableDocument.Enums;
 
 namespace PixiEditor.ChangeableDocument.Changes.Root;
@@ -23,7 +21,7 @@ internal class SymmetryAxisPosition_UpdateableChange : UpdateableChange
         newPos = pos;
     }
 
-    public override void Initialize(Document target)
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
         originalPos = direction switch
         {
@@ -31,6 +29,7 @@ internal class SymmetryAxisPosition_UpdateableChange : UpdateableChange
             SymmetryAxisDirection.Vertical => target.VerticalSymmetryAxisX,
             _ => throw new NotImplementedException(),
         };
+        return new Success();
     }
 
     private void SetPosition(Document target, int position)

+ 5 - 11
src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryAxisState_Change.cs

@@ -1,6 +1,4 @@
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Root;
+using PixiEditor.ChangeableDocument.ChangeInfos.Root;
 using PixiEditor.ChangeableDocument.Enums;
 
 namespace PixiEditor.ChangeableDocument.Changes.Root;
@@ -17,7 +15,7 @@ internal class SymmetryAxisState_Change : Change
         this.newEnabled = enabled;
     }
 
-    public override void Initialize(Document target)
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
     {
         originalEnabled = direction switch
         {
@@ -25,6 +23,9 @@ internal class SymmetryAxisState_Change : Change
             SymmetryAxisDirection.Vertical => target.VerticalSymmetryAxisEnabled,
             _ => throw new NotImplementedException(),
         };
+        if (originalEnabled == newEnabled)
+            return new Error();
+        return new Success();
     }
 
     private void SetState(Document target, bool state)
@@ -39,11 +40,6 @@ internal class SymmetryAxisState_Change : Change
 
     public override IChangeInfo? Apply(Document target, out bool ignoreInUndo)
     {
-        if (originalEnabled == newEnabled)
-        {
-            ignoreInUndo = true;
-            return null;
-        }
         SetState(target, newEnabled);
         ignoreInUndo = false;
         return new SymmetryAxisState_ChangeInfo() { Direction = direction };
@@ -51,8 +47,6 @@ internal class SymmetryAxisState_Change : Change
 
     public override IChangeInfo? Revert(Document target)
     {
-        if (originalEnabled == newEnabled)
-            return null;
         SetState(target, originalEnabled);
         return new SymmetryAxisState_ChangeInfo() { Direction = direction };
     }

+ 7 - 4
src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

@@ -1,6 +1,4 @@
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 using PixiEditor.ChangeableDocument.Enums;
 
 namespace PixiEditor.ChangeableDocument.Changes.Structure;
@@ -22,7 +20,12 @@ internal class CreateStructureMember_Change : Change
         newMemberGuid = newGuid;
     }
 
-    public override void Initialize(Document target) { }
+    public override OneOf<Success, Error> InitializeAndValidate(Document target)
+    {
+        if (target.FindMember(parentFolderGuid) is null)
+            return new Error();
+        return new Success();
+    }
 
     public override IChangeInfo Apply(Document document, out bool ignoreInUndo)
     {

+ 7 - 6
src/PixiEditor.ChangeableDocument/Changes/Structure/DeleteStructureMember_Change.cs

@@ -1,6 +1,4 @@
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 
 namespace PixiEditor.ChangeableDocument.Changes.Structure;
 
@@ -17,13 +15,16 @@ internal class DeleteStructureMember_Change : Change
         this.memberGuid = memberGuid;
     }
 
-    public override void Initialize(Document document)
+    public override OneOf<Success, Error> InitializeAndValidate(Document document)
     {
-        var (member, parent) = document.FindChildAndParentOrThrow(memberGuid);
+        var (member, parent) = document.FindChildAndParent(memberGuid);
+        if (member is null || parent is null)
+            return new Error();
 
         originalIndex = parent.Children.IndexOf(member);
         parentGuid = parent.GuidValue;
         savedCopy = member.Clone();
+        return new Success();
     }
 
     public override IChangeInfo Apply(Document document, out bool ignoreInUndo)
@@ -45,6 +46,6 @@ internal class DeleteStructureMember_Change : Change
 
     public override void Dispose()
     {
-        savedCopy!.Dispose();
+        savedCopy?.Dispose();
     }
 }

+ 7 - 5
src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs

@@ -1,6 +1,4 @@
-using PixiEditor.ChangeableDocument.Changeables;
-using PixiEditor.ChangeableDocument.ChangeInfos;
-using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 
 namespace PixiEditor.ChangeableDocument.Changes.Structure;
 
@@ -22,11 +20,15 @@ internal class MoveStructureMember_Change : Change
         this.targetFolderIndex = targetFolderIndex;
     }
 
-    public override void Initialize(Document document)
+    public override OneOf<Success, Error> InitializeAndValidate(Document document)
     {
-        var (member, curFolder) = document.FindChildAndParentOrThrow(memberGuid);
+        var (member, curFolder) = document.FindChildAndParent(memberGuid);
+        var targetFolder = document.FindMember(targetFolderGuid);
+        if (member is null || curFolder is null || targetFolder is not Folder)
+            return new Error();
         originalFolderGuid = curFolder.GuidValue;
         originalFolderIndex = curFolder.Children.IndexOf(member);
+        return new Success();
     }
 
     private static void Move(Document document, Guid memberGuid, Guid targetFolderGuid, int targetIndex)

+ 63 - 27
src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs

@@ -1,8 +1,7 @@
-using PixiEditor.ChangeableDocument.Actions;
+using System.Diagnostics;
+using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Undo;
-using PixiEditor.ChangeableDocument.Changeables;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
-using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.ChangeableDocument.Changes;
 
 namespace ChangeableDocument;
@@ -14,7 +13,7 @@ public class DocumentChangeTracker : IDisposable
     private bool running = false;
     public IReadOnlyDocument Document => document;
 
-    private UpdateableChange? activeChange = null;
+    private UpdateableChange? activeUpdateableChange = null;
     private List<Change>? activePacket = null;
 
     private Stack<List<Change>> undoStack = new();
@@ -30,7 +29,7 @@ public class DocumentChangeTracker : IDisposable
 
         document.Dispose();
 
-        activeChange?.Dispose();
+        activeUpdateableChange?.Dispose();
 
         if (activePacket != null)
             foreach (var change in activePacket)
@@ -97,8 +96,11 @@ public class DocumentChangeTracker : IDisposable
     {
         if (undoStack.Count == 0)
             return new List<IChangeInfo?>();
-        if (activePacket is not null || activeChange is not null)
-            throw new InvalidOperationException("Cannot undo while there is an active updateable change or an unfinished undo packet");
+        if (activePacket is not null || activeUpdateableChange is not null)
+        {
+            Trace.WriteLine("Attempted to undo while there is an active updateable change or an unfinished undo packet");
+            return new List<IChangeInfo?>();
+        }
         List<IChangeInfo?> changeInfos = new();
         List<Change> changePacket = undoStack.Pop();
 
@@ -113,8 +115,11 @@ public class DocumentChangeTracker : IDisposable
     {
         if (redoStack.Count == 0)
             return new List<IChangeInfo?>();
-        if (activePacket is not null || activeChange is not null)
-            throw new InvalidOperationException("Cannot redo while there is an active updateable change or an unfinished undo packet");
+        if (activePacket is not null || activeUpdateableChange is not null)
+        {
+            Trace.WriteLine("Attempted to redo while there is an active updateable change or an unfinished undo packet");
+            return new List<IChangeInfo?>();
+        }
         List<IChangeInfo?> changeInfos = new();
         List<Change> changePacket = redoStack.Pop();
 
@@ -127,8 +132,11 @@ public class DocumentChangeTracker : IDisposable
 
     private void DeleteAllChanges()
     {
-        if (activeChange is not null || activePacket is not null)
-            throw new InvalidOperationException("Cannot delete all changes while there is an active updateable change or an unfinished undo packet");
+        if (activeUpdateableChange is not null || activePacket is not null)
+        {
+            Trace.WriteLine("Attempted to delete all changes while there is an active updateable change or an unfinished undo packet");
+            return;
+        }
         foreach (var changesToDispose in redoStack)
             foreach (var changeToDispose in changesToDispose)
                 changeToDispose.Dispose();
@@ -141,10 +149,20 @@ public class DocumentChangeTracker : IDisposable
 
     private IChangeInfo? ProcessMakeChangeAction(IMakeChangeAction act)
     {
-        if (activeChange is not null)
-            throw new InvalidOperationException("Can't make a change while another change is active");
+        if (activeUpdateableChange is not null)
+        {
+            Trace.WriteLine($"Attempted to execute make change action {act} while {activeUpdateableChange} is active");
+            return null;
+        }
         var change = act.CreateCorrespondingChange();
-        change.Initialize(document);
+        var validationResult = change.InitializeAndValidate(document);
+        if (validationResult.IsT1)
+        {
+            Trace.WriteLine($"Change {change} failed validation");
+            change.Dispose();
+            return null;
+        }
+
         var info = change.Apply(document, out bool ignoreInUndo);
         if (!ignoreInUndo)
             AddToUndo(change);
@@ -155,28 +173,46 @@ public class DocumentChangeTracker : IDisposable
 
     private IChangeInfo? ProcessStartOrUpdateChangeAction(IStartOrUpdateChangeAction act)
     {
-        if (activeChange is null)
+        if (activeUpdateableChange is null)
         {
-            activeChange = act.CreateCorrespondingChange();
-            activeChange.Initialize(document);
+            var newChange = act.CreateCorrespondingChange();
+            var validationResult = newChange.InitializeAndValidate(document);
+            if (validationResult.IsT1)
+            {
+                Trace.WriteLine($"Change {newChange} failed validation");
+                newChange.Dispose();
+                return null;
+            }
+            activeUpdateableChange = newChange;
         }
-        act.UpdateCorrespodingChange(activeChange);
-        return activeChange.ApplyTemporarily(document);
+        else if (!act.IsChangeTypeMatching(activeUpdateableChange))
+        {
+            Trace.WriteLine($"Tried to start or update a change using action {act} while a change of type {activeUpdateableChange} is active");
+            return null;
+        }
+        act.UpdateCorrespodingChange(activeUpdateableChange);
+        return activeUpdateableChange.ApplyTemporarily(document);
     }
 
     private IChangeInfo? ProcessEndChangeAction(IEndChangeAction act)
     {
-        if (activeChange is null)
-            throw new InvalidOperationException("Can't end a change: no changes are active");
-        if (!act.IsChangeTypeMatching(activeChange))
-            throw new InvalidOperationException($"Trying to end a change with an action of type {act.GetType()} while a change of type {activeChange.GetType()} is active");
+        if (activeUpdateableChange is null)
+        {
+            Trace.WriteLine($"Attempted to end a change using action {act} while no changes are active");
+            return null;
+        }
+        if (!act.IsChangeTypeMatching(activeUpdateableChange))
+        {
+            Trace.WriteLine($"Trying to end a change with an action {act} while change {activeUpdateableChange} is active");
+            return null;
+        }
 
-        var info = activeChange.Apply(document, out bool ignoreInUndo);
+        var info = activeUpdateableChange.Apply(document, out bool ignoreInUndo);
         if (!ignoreInUndo)
-            AddToUndo(activeChange);
+            AddToUndo(activeUpdateableChange);
         else
-            activeChange.Dispose();
-        activeChange = null;
+            activeUpdateableChange.Dispose();
+        activeUpdateableChange = null;
         return info;
     }
 

+ 7 - 1
src/PixiEditor.ChangeableDocument/GlobalUsings.cs

@@ -1 +1,7 @@
-global using PixiEditor.ChangeableDocument.Actions.Attributes;
+global using ChunkyImageLib;
+global using ChunkyImageLib.DataHolders;
+global using OneOf;
+global using OneOf.Types;
+global using PixiEditor.ChangeableDocument.Actions.Attributes;
+global using PixiEditor.ChangeableDocument.Changeables;
+global using PixiEditor.ChangeableDocument.ChangeInfos;

+ 1 - 1
src/README.md

@@ -45,7 +45,7 @@ Decouples the state of a document from the UI.
 - ChangeableDocument/Renderer
     - [x] Basic Action->Change->ChangeInfo pipeline
     - [x] Code generation for Action/ChangeInfo boilerplate
-    - [ ] Ignore invalid inputs
+    - [x] Ignore invalid inputs
     - Undo handling
         - [x] UpdateableChange class for changes requiring preview
         - [x] Handling for changes that don't change anything