Browse Source

Creating shape now takes one undo step

flabbet 10 months ago
parent
commit
bc1308aba6

+ 36 - 29
src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs

@@ -20,18 +20,18 @@ public class DocumentChangeTracker : IDisposable
         {
             if (!undoStack.Any())
                 return null;
-            List<Change> list = undoStack.Peek();
-            if (list.Count == 0)
+            var list = undoStack.Peek();
+            if (list.changes.Count == 0)
                 return null;
-            return list[^1].ChangeGuid;
+            return list.changes[^1].ChangeGuid;
         }
     }
 
     private UpdateableChange? activeUpdateableChange = null;
     private List<Change>? activePacket = null;
 
-    private Stack<List<Change>> undoStack = new();
-    private Stack<List<Change>> redoStack = new();
+    private Stack<(ActionSource source, List<Change> changes)> undoStack = new();
+    private Stack<(ActionSource source, List<Change> changes)> redoStack = new();
 
     public void Dispose()
     {
@@ -53,13 +53,13 @@ public class DocumentChangeTracker : IDisposable
 
         foreach (var list in undoStack)
         {
-            foreach (var change in list)
+            foreach (var change in list.changes)
                 change.Dispose();
         }
 
         foreach (var list in redoStack)
         {
-            foreach (var change in list)
+            foreach (var change in list.changes)
                 change.Dispose();
         }
     }
@@ -77,14 +77,14 @@ public class DocumentChangeTracker : IDisposable
 
         foreach (var changesToDispose in redoStack)
         {
-            foreach (var changeToDispose in changesToDispose)
+            foreach (var changeToDispose in changesToDispose.changes)
                 changeToDispose.Dispose();
         }
 
         redoStack.Clear();
     }
 
-    private void CompletePacket()
+    private void CompletePacket(ActionSource source)
     {
         if (activePacket is null)
             return;
@@ -92,24 +92,25 @@ public class DocumentChangeTracker : IDisposable
         // maybe merge with previous
         if (activePacket.Count == 1 &&
             undoStack.Count > 0 &&
-            IsHomologous(undoStack.Peek()) &&
-            undoStack.Peek()[^1].IsMergeableWith(activePacket[0]))
+            (undoStack.Peek().source == ActionSource.Automated ||
+            (IsHomologous(undoStack.Peek()) &&
+            undoStack.Peek().changes[^1].IsMergeableWith(activePacket[0]))))
         {
-            undoStack.Peek().Add(activePacket[0]);
+            undoStack.Peek().changes.Add(activePacket[0]);
         }
         else
         {
-            undoStack.Push(activePacket);
+            undoStack.Push((source, activePacket));
         }
 
         activePacket = null;
     }
 
-    private bool IsHomologous(List<Change> changes)
+    private bool IsHomologous((ActionSource source, List<Change> changes) changes)
     {
-        for (int i = 1; i < changes.Count; i++)
+        for (int i = 1; i < changes.changes.Count; i++)
         {
-            if (!changes[i].IsMergeableWith(changes[i - 1]))
+            if (!changes.changes[i].IsMergeableWith(changes.changes[i - 1]))
                 return false;
         }
         return true;
@@ -125,11 +126,11 @@ public class DocumentChangeTracker : IDisposable
             return new List<IChangeInfo>();
         }
         List<IChangeInfo> changeInfos = new();
-        List<Change> changePacket = undoStack.Pop();
+        var changePacket = undoStack.Pop();
 
-        for (int i = changePacket.Count - 1; i >= 0; i--)
+        for (int i = changePacket.changes.Count - 1; i >= 0; i--)
         {
-            changePacket[i].Revert(document).Switch(
+            changePacket.changes[i].Revert(document).Switch(
                 (None _) => { },
                 (IChangeInfo info) => changeInfos.Add(info),
                 (List<IChangeInfo> infos) => changeInfos.AddRange(infos));
@@ -149,11 +150,11 @@ public class DocumentChangeTracker : IDisposable
             return new List<IChangeInfo>();
         }
         List<IChangeInfo> changeInfos = new();
-        List<Change> changePacket = redoStack.Pop();
+        var changePacket = redoStack.Pop();
 
-        for (int i = 0; i < changePacket.Count; i++)
+        for (int i = 0; i < changePacket.changes.Count; i++)
         {
-            changePacket[i].Apply(document, false, out _).Switch(
+            changePacket.changes[i].Apply(document, false, out _).Switch(
                 (None _) => { },
                 (IChangeInfo info) => changeInfos.Add(info),
                 (List<IChangeInfo> infos) => changeInfos.AddRange(infos));
@@ -172,13 +173,13 @@ public class DocumentChangeTracker : IDisposable
         }
         foreach (var changesToDispose in redoStack)
         {
-            foreach (var changeToDispose in changesToDispose)
+            foreach (var changeToDispose in changesToDispose.changes)
                 changeToDispose.Dispose();
         }
 
         foreach (var changesToDispose in undoStack)
         {
-            foreach (var changeToDispose in changesToDispose)
+            foreach (var changeToDispose in changesToDispose.changes)
                 changeToDispose.Dispose();
         }
 
@@ -255,7 +256,7 @@ public class DocumentChangeTracker : IDisposable
         return info;
     }
 
-    private List<IChangeInfo?> ProcessActionList(IReadOnlyList<IAction> actions)
+    private List<IChangeInfo?> ProcessActionList(IReadOnlyList<(ActionSource, IAction)> actions)
     {
         List<IChangeInfo?> changeInfos = new();
         void AddInfo(OneOf<None, IChangeInfo, List<IChangeInfo>> info) =>
@@ -266,7 +267,7 @@ public class DocumentChangeTracker : IDisposable
 
         foreach (var action in actions)
         {
-            switch (action)
+            switch (action.Item2)
             {
                 case IMakeChangeAction act:
                     AddInfo(ProcessMakeChangeAction(act));
@@ -284,7 +285,7 @@ public class DocumentChangeTracker : IDisposable
                     AddInfo(Redo());
                     break;
                 case ChangeBoundary_Action:
-                    CompletePacket();
+                    CompletePacket(action.Item1);
                     break;
                 case DeleteRecordedChanges_Action:
                     DeleteAllChanges();
@@ -298,7 +299,7 @@ public class DocumentChangeTracker : IDisposable
         return changeInfos;
     }
 
-    public async Task<List<IChangeInfo?>> ProcessActions(IReadOnlyList<IAction> actions)
+    public async Task<List<IChangeInfo?>> ProcessActions(List<(ActionSource, IAction)> actions)
     {
         if (disposed)
             throw new ObjectDisposedException(nameof(DocumentChangeTracker));
@@ -310,7 +311,7 @@ public class DocumentChangeTracker : IDisposable
         return result;
     }
 
-    public List<IChangeInfo?> ProcessActionsSync(IReadOnlyList<IAction> actions)
+    public List<IChangeInfo?> ProcessActionsSync(IReadOnlyList<(ActionSource, IAction)> actions)
     {
         if (disposed)
             throw new ObjectDisposedException(nameof(DocumentChangeTracker));
@@ -322,3 +323,9 @@ public class DocumentChangeTracker : IDisposable
         return result;
     }
 }
+
+public enum ActionSource
+{
+    User,
+    Automated
+}

+ 26 - 15
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -1,13 +1,10 @@
-using System.Collections.Generic;
-using System.Linq;
-using Avalonia.Platform;
-using Avalonia.Threading;
+using Avalonia.Threading;
+using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.DrawingApi.Core.Bridge;
-using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DocumentPassthroughActions;
 using PixiEditor.Models.Handlers;
@@ -21,7 +18,7 @@ internal class ActionAccumulator
 {
     private bool executing = false;
 
-    private List<IAction> queuedActions = new();
+    private List<(ActionSource source, IAction action)> queuedActions = new();
     private IDocument document;
     private DocumentInternalParts internals;
 
@@ -39,14 +36,28 @@ internal class ActionAccumulator
 
     public void AddFinishedActions(params IAction[] actions)
     {
-        queuedActions.AddRange(actions);
-        queuedActions.Add(new ChangeBoundary_Action());
+        foreach (var action in actions)
+        {
+            queuedActions.Add((ActionSource.User, action));
+        }
+        
+        queuedActions.Add((ActionSource.Automated, new ChangeBoundary_Action()));
         TryExecuteAccumulatedActions();
     }
 
     public void AddActions(params IAction[] actions)
     {
-        queuedActions.AddRange(actions);
+        foreach (var action in actions)
+        {
+            queuedActions.Add((ActionSource.User, action));
+        }
+        
+        TryExecuteAccumulatedActions();
+    }
+    
+    public void AddActions(ActionSource source, IAction action)
+    {
+        queuedActions.Add((source, action));
         TryExecuteAccumulatedActions();
     }
 
@@ -67,13 +78,13 @@ internal class ActionAccumulator
         {
             // select actions to be processed
             var toExecute = queuedActions;
-            queuedActions = new List<IAction>();
+            queuedActions = new();
 
             // pass them to changeabledocument for processing
             List<IChangeInfo?> changes;
             if (AreAllPassthrough(toExecute))
             {
-                changes = toExecute.Select(a => (IChangeInfo?)a).ToList();
+                changes = toExecute.Select(a => (IChangeInfo?)a.action).ToList();
             }
             else
             {
@@ -83,8 +94,8 @@ internal class ActionAccumulator
             // update viewmodels based on changes
             List<IChangeInfo> optimizedChanges = ChangeInfoListOptimizer.Optimize(changes);
             bool undoBoundaryPassed =
-                toExecute.Any(static action => action is ChangeBoundary_Action or Redo_Action or Undo_Action);
-            bool viewportRefreshRequest = toExecute.Any(static action => action is RefreshViewport_PassthroughAction);
+                toExecute.Any(static action => action.action is ChangeBoundary_Action or Redo_Action or Undo_Action);
+            bool viewportRefreshRequest = toExecute.Any(static action => action.action is RefreshViewport_PassthroughAction);
             foreach (IChangeInfo info in optimizedChanges)
             {
                 internals.Updater.ApplyChangeFromChangeInfo(info);
@@ -133,11 +144,11 @@ internal class ActionAccumulator
         executing = false;
     }
 
-    private bool AreAllPassthrough(List<IAction> actions)
+    private bool AreAllPassthrough(List<(ActionSource, IAction)> actions)
     {
         foreach (var action in actions)
         {
-            if (action is not IChangeInfo)
+            if (action.Item2 is not IChangeInfo)
                 return false;
         }
 

+ 5 - 4
src/PixiEditor/Models/DocumentModels/DocumentStructureHelper.cs

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using PixiEditor.ChangeableDocument;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
@@ -98,7 +99,7 @@ internal class DocumentStructureHelper
         throw new ArgumentException($"Unknown member type: {type}");
     }
 
-    public Guid? CreateNewStructureMember(Type structureMemberType, string? name, bool finish)
+    public Guid? CreateNewStructureMember(Type structureMemberType, string? name, ActionSource source)
     {
         Guid guid = Guid.NewGuid();
         var selectedMember = doc.SelectedStructureMember;
@@ -109,13 +110,13 @@ internal class DocumentStructureHelper
         if (parent is null)
             parent = doc.NodeGraphHandler.OutputNode;
 
-        internals.ActionAccumulator.AddActions(new CreateStructureMember_Action(parent.Id, guid, structureMemberType));
+        internals.ActionAccumulator.AddActions(source, new CreateStructureMember_Action(parent.Id, guid, structureMemberType));
         name ??= GetUniqueName(
             structureMemberType.IsAssignableTo(typeof(LayerNode))
                 ? new LocalizedString("NEW_LAYER")
                 : new LocalizedString("NEW_FOLDER"), parent);
-        internals.ActionAccumulator.AddActions(new StructureMemberName_Action(guid, name));
-        if (finish)
+        internals.ActionAccumulator.AddActions(source, new StructureMemberName_Action(guid, name));
+        if (source == ActionSource.User)
             internals.ActionAccumulator.AddFinishedActions();
         return guid;
     }

+ 10 - 4
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -1,5 +1,6 @@
 using System.Collections.Immutable;
 using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.ChangeableDocument.Actions.Generated;
@@ -177,14 +178,14 @@ internal class DocumentOperationsModule : IDocumentOperations
         return Internals.StructureHelper.CreateNewStructureMember(type, name, finish);
     }
 
-    public Guid? CreateStructureMember(Type structureMemberType, string? name = null, bool finish = true)
+    public Guid? CreateStructureMember(Type structureMemberType, ActionSource source, string? name = null)
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return null;
 
         Internals.ChangeController.TryStopActiveExecutor();
 
-        return Internals.StructureHelper.CreateNewStructureMember(structureMemberType, name, finish);
+        return Internals.StructureHelper.CreateNewStructureMember(structureMemberType, name, source);
     }
 
     /// <summary>
@@ -441,12 +442,17 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// </summary>
     public void Redo()
     {
-        if (Internals.ChangeController.IsChangeOfTypeActive<IMidChangeUndoableExecutor>())
+        IMidChangeUndoableExecutor executor = Internals.ChangeController.TryGetExecutorFeature<IMidChangeUndoableExecutor>();
+        if (executor is { CanRedo: true })
         {
-            Internals.ChangeController.MidChangeRedoInlet();
+            executor.OnMidChangeRedo();
             return;
         }
+        
+        if(Internals.ChangeController.IsBlockingChangeActive)
+            return;
 
+        Internals.ChangeController.TryStopActiveExecutor();
         Internals.ActionAccumulator.AddActions(new Redo_Action());
     }
 

+ 1 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ComplexShapeToolExecutor.cs

@@ -33,6 +33,7 @@ internal abstract class ComplexShapeToolExecutor<T> : SimpleShapeToolExecutor wh
     private IColorsHandler? colorsVM;
 
     public override bool CanUndo => document.TransformHandler.HasUndo;
+    public override bool CanRedo => document.TransformHandler.HasRedo;
 
     public override ExecutionState Start()
     {

+ 1 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/IMidChangeUndoableExecutor.cs

@@ -5,4 +5,5 @@ public interface IMidChangeUndoableExecutor : IExecutorFeature
     public void OnMidChangeUndo();
     public void OnMidChangeRedo();
     public bool CanUndo { get; }
+    public bool CanRedo { get; }
 }

+ 2 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs

@@ -27,7 +27,8 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
     private ILineToolbar? toolbar;
 
     public override bool CanUndo => document.LineToolOverlayHandler.HasUndo; 
-
+    public override bool CanRedo => document.LineToolOverlayHandler.HasRedo;
+    
     public override ExecutionState Start()
     {
         if (base.Start() == ExecutionState.Error)

+ 2 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs

@@ -75,7 +75,8 @@ internal class PasteImageExecutor : UpdateableChangeExecutor, ITransformableExec
     public void OnMidChangeUndo() => document!.TransformHandler.Undo();
 
     public void OnMidChangeRedo() => document!.TransformHandler.Redo();
-    public bool CanUndo => document!.TransformHandler.HasUndo; 
+    public bool CanUndo => document!.TransformHandler.HasUndo;
+    public bool CanRedo => document!.TransformHandler.HasRedo;
 
     public void OnTransformApplied()
     {

+ 1 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SimpleShapeToolExecutor.cs

@@ -208,6 +208,7 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
     public abstract void OnMidChangeUndo();
     public abstract void OnMidChangeRedo();
     public abstract bool CanUndo { get; } 
+    public abstract bool CanRedo { get; }
 
     public bool IsFeatureEnabled(IExecutorFeature feature)
     {

+ 1 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -134,6 +134,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
     public void OnMidChangeRedo() => document!.TransformHandler.Redo();
     public bool CanUndo => document!.TransformHandler.HasUndo; 
+    public bool CanRedo => document!.TransformHandler.HasRedo;
 
     public void OnTransformApplied()
     {

+ 1 - 0
src/PixiEditor/Models/Handlers/ILineOverlayHandler.cs

@@ -11,4 +11,5 @@ internal interface ILineOverlayHandler
     public bool Redo();
     public void Show(VecD startPos, VecD endPos, bool showApplyButton);
     public bool HasUndo { get; }
+    public bool HasRedo { get; }
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/ITransformHandler.cs

@@ -14,4 +14,5 @@ internal interface ITransformHandler : IHandler
     public bool Redo();
     public bool Nudge(VecD distance);
     public bool HasUndo { get; }
+    public bool HasRedo { get; }
 }

+ 1 - 0
src/PixiEditor/ViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs

@@ -180,6 +180,7 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
     }
 
     public bool HasUndo => undoStack is not null && undoStack.UndoCount > 0; 
+    public bool HasRedo => undoStack is not null && undoStack.RedoCount > 0;
 
     public void HideTransform()
     {

+ 2 - 1
src/PixiEditor/ViewModels/Document/TransformOverlays/LineToolOverlayViewModel.cs

@@ -80,7 +80,8 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
         ShowApplyButton = showApplyButton;
     }
 
-    public bool HasUndo => undoStack is not null && undoStack.UndoCount > 0; 
+    public bool HasUndo => undoStack is not null && undoStack.UndoCount > 0;
+    public bool HasRedo => undoStack is not null && undoStack.RedoCount > 0; 
 
     public void Hide()
     {

+ 1 - 0
src/PixiEditor/ViewModels/Document/TransformOverlays/TransformOverlayUndoStack.cs

@@ -31,6 +31,7 @@ internal class TransformOverlayUndoStack<TState> where TState : struct
     public TState? PeekCurrent() => current?.State;
     
     public int UndoCount => undoStack.Count;
+    public int RedoCount => redoStack.Count; 
 
     public TState? Undo()
     {

+ 3 - 2
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -9,6 +9,7 @@ using Avalonia.Media;
 using Avalonia.Media.Imaging;
 using Avalonia.Platform;
 using Avalonia.Platform.Storage;
+using PixiEditor.ChangeableDocument;
 using PixiEditor.Helpers.Converters;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.ChangeableDocument.Enums;
@@ -126,12 +127,12 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
         doc.Operations.CreateStructureMember(StructureMemberType.Layer);
     }
 
-    public Guid? NewLayer(Type layerType, string? name = null, bool finish = true)
+    public Guid? NewLayer(Type layerType, ActionSource source, string? name = null)
     {
         if (Owner.DocumentManagerSubViewModel.ActiveDocument is not { } doc)
             return null;
         
-        return doc.Operations.CreateStructureMember(layerType, name, finish);
+        return doc.Operations.CreateStructureMember(layerType, source, name);
     }
 
     [Evaluator.CanExecute("PixiEditor.Layer.CanCreateNewMember")]

+ 8 - 2
src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -4,6 +4,7 @@ using System.ComponentModel;
 using System.Linq;
 using Avalonia.Input;
 using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.Models.Preferences;
 using PixiEditor.DrawingApi.Core.Numerics;
@@ -373,7 +374,9 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
 
         if (ActiveTool is not { CanBeUsedOnActiveLayer: true })
         {
-            Guid? createdLayer = Owner.LayersSubViewModel.NewLayer(ActiveTool.LayerTypeToCreateOnEmptyUse, 
+            Guid? createdLayer = Owner.LayersSubViewModel.NewLayer(
+                ActiveTool.LayerTypeToCreateOnEmptyUse,
+                ActionSource.Automated,
                 ActiveTool.DefaultNewLayerName);
             if (createdLayer is not null)
             {
@@ -386,7 +389,10 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         if (waitForChange)
         {
             Owner.DocumentManagerSubViewModel.ActiveDocument.Operations
-                .InvokeCustomAction(() => ActiveTool.UseTool(canvasPos));
+                .InvokeCustomAction(() =>
+                {
+                    ActiveTool.UseTool(canvasPos);
+                });
         }
         else
         {