UndoManager.cs 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. using PixiEditor.Models.Undo;
  2. using PixiEditor.ViewModels;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.Linq;
  7. using System.Reflection;
  8. namespace PixiEditor.Models.Controllers
  9. {
  10. [DebuggerDisplay("{UndoStack.Count} undo steps, {RedoStack.Count} redo step(s)")]
  11. public class UndoManager : IDisposable
  12. {
  13. private bool lastChangeWasUndo;
  14. private PropertyInfo newUndoChangeBlockedProperty;
  15. public Stack<Change> UndoStack { get; set; } = new Stack<Change>();
  16. public Stack<Change> RedoStack { get; set; } = new Stack<Change>();
  17. public bool CanUndo => UndoStack.Count > 0;
  18. public bool CanRedo => RedoStack.Count > 0;
  19. public object MainRoot { get; set; }
  20. public UndoManager()
  21. {
  22. if (ViewModelMain.Current != null && ViewModelMain.Current.UndoSubViewModel != null)
  23. {
  24. MainRoot = ViewModelMain.Current.UndoSubViewModel;
  25. }
  26. }
  27. public UndoManager(object mainRoot)
  28. {
  29. MainRoot = mainRoot;
  30. }
  31. /// <summary>
  32. /// Adds property change to UndoStack.
  33. /// </summary>
  34. public void AddUndoChange(Change change, bool invokedInsideSetter = false)
  35. {
  36. if (change.Property != null && (ChangeIsBlockedProperty(change) && invokedInsideSetter == true))
  37. {
  38. newUndoChangeBlockedProperty = null;
  39. return;
  40. }
  41. lastChangeWasUndo = false;
  42. // Clears RedoStack if last move wasn't redo or undo and if redo stack is greater than 0.
  43. if (lastChangeWasUndo == false && RedoStack.Count > 0)
  44. {
  45. RedoStack.Clear();
  46. }
  47. change.Root ??= MainRoot;
  48. UndoStack.Push(change);
  49. }
  50. /// <summary>
  51. /// Sets top property in UndoStack to Old Value.
  52. /// </summary>
  53. public void Undo()
  54. {
  55. lastChangeWasUndo = true;
  56. Change change = UndoStack.Pop();
  57. if (change.ReverseProcess == null)
  58. {
  59. SetPropertyValue(GetChangeRoot(change), change.Property, change.OldValue);
  60. }
  61. else
  62. {
  63. change.ReverseProcess(change.ReverseProcessArguments);
  64. }
  65. RedoStack.Push(change);
  66. }
  67. /// <summary>
  68. /// Sets top property from RedoStack to old value.
  69. /// </summary>
  70. public void Redo()
  71. {
  72. lastChangeWasUndo = true;
  73. Change change = RedoStack.Pop();
  74. if (change.Process == null)
  75. {
  76. SetPropertyValue(GetChangeRoot(change), change.Property, change.NewValue);
  77. }
  78. else
  79. {
  80. change.Process(change.ProcessArguments);
  81. }
  82. UndoStack.Push(change);
  83. }
  84. /// <summary>
  85. /// Merges multiple undo changes into one.
  86. /// </summary>
  87. /// <param name="amount">Amount of changes to squash.</param>
  88. public void SquashUndoChanges(int amount)
  89. {
  90. string description = UndoStack.ElementAt(UndoStack.Count - amount).Description;
  91. if (string.IsNullOrEmpty(description))
  92. {
  93. description = $"Squash {amount} undo changes.";
  94. }
  95. SquashUndoChanges(amount, description);
  96. }
  97. /// <summary>
  98. /// Merges multiple undo changes into one.
  99. /// </summary>
  100. /// <param name="amount">Amount of changes to squash.</param>
  101. /// <param name="description">Final change description.</param>
  102. public void SquashUndoChanges(int amount, string description)
  103. {
  104. Change[] changes = new Change[amount];
  105. for (int i = 0; i < amount; i++)
  106. {
  107. changes[i] = UndoStack.Pop();
  108. }
  109. Action<object[]> reverseProcess = (object[] props) =>
  110. {
  111. foreach (var prop in props)
  112. {
  113. Change change = (Change)prop;
  114. if (change.ReverseProcess == null)
  115. {
  116. SetPropertyValue(GetChangeRoot(change), change.Property, change.OldValue);
  117. }
  118. else
  119. {
  120. change.ReverseProcess(change.ReverseProcessArguments);
  121. }
  122. }
  123. };
  124. Action<object[]> process = (object[] props) =>
  125. {
  126. foreach (var prop in props.Reverse())
  127. {
  128. Change change = (Change)prop;
  129. if (change.Process == null)
  130. {
  131. SetPropertyValue(GetChangeRoot(change), change.Property, change.NewValue);
  132. }
  133. else
  134. {
  135. change.Process(change.ProcessArguments);
  136. }
  137. }
  138. };
  139. Change change = new(reverseProcess, changes, process, changes, description);
  140. AddUndoChange(change);
  141. }
  142. public void Dispose()
  143. {
  144. foreach (Change change in UndoStack.Concat(RedoStack))
  145. {
  146. change.Dispose();
  147. }
  148. GC.SuppressFinalize(this);
  149. }
  150. private bool ChangeIsBlockedProperty(Change change)
  151. {
  152. return (change.Root != null || change.FindRootProcess != null)
  153. && GetProperty(GetChangeRoot(change), change.Property).Item1 == newUndoChangeBlockedProperty;
  154. }
  155. private object GetChangeRoot(Change change)
  156. {
  157. return change.FindRootProcess != null ? change.FindRootProcess(change.FindRootProcessArgs) : change.Root;
  158. }
  159. private void SetPropertyValue(object target, string propName, object value)
  160. {
  161. var properties = GetProperty(target, propName);
  162. PropertyInfo propertyToSet = properties.Item1;
  163. newUndoChangeBlockedProperty = propertyToSet;
  164. propertyToSet.SetValue(properties.Item2, value, null);
  165. }
  166. /// <summary>
  167. /// Gets property info for propName from target. Supports '.' format.
  168. /// </summary>
  169. /// <param name="target">A object where target can be found.</param>
  170. /// <param name="propName">Name of property to get, supports nested property.</param>
  171. /// <returns>PropertyInfo about property and target object where property can be found.</returns>
  172. private Tuple<PropertyInfo, object> GetProperty(object target, string propName)
  173. {
  174. string[] bits = propName.Split('.');
  175. for (int i = 0; i < bits.Length - 1; i++)
  176. {
  177. PropertyInfo propertyToGet = target.GetType().GetProperty(bits[i]);
  178. target = propertyToGet.GetValue(target, null);
  179. }
  180. return new Tuple<PropertyInfo, object>(target.GetType().GetProperty(bits.Last()), target);
  181. }
  182. }
  183. }