Add project files.
This commit is contained in:
215
Examples/Nodify.Shared/UndoRedo/ActionsHistory.cs
Normal file
215
Examples/Nodify.Shared/UndoRedo/ActionsHistory.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Nodify.UndoRedo
|
||||
{
|
||||
public interface IActionsHistory : INotifyPropertyChanged
|
||||
{
|
||||
int MaxSize { get; set; }
|
||||
bool CanUndo { get; }
|
||||
bool CanRedo { get; }
|
||||
bool IsEnabled { get; set; }
|
||||
IAction? Current { get; }
|
||||
|
||||
void Undo();
|
||||
void Redo();
|
||||
|
||||
void Clear();
|
||||
|
||||
/// <summary>
|
||||
/// All future modifications will be merged together to create a single history item until batch is disposed.
|
||||
/// </summary>
|
||||
IDisposable Batch(string? label = default);
|
||||
|
||||
void Record(IAction action);
|
||||
|
||||
/// <summary>
|
||||
/// All future modifications will be merged together to create a single history item until history is resumed.
|
||||
/// </summary>
|
||||
void Pause(string? label = default);
|
||||
|
||||
/// <summary>Each future modifications will create a new history item.</summary>
|
||||
void Resume();
|
||||
}
|
||||
|
||||
public interface IAction
|
||||
{
|
||||
string? Label { get; }
|
||||
|
||||
void Execute();
|
||||
void Undo();
|
||||
}
|
||||
|
||||
public static class ActionsHistoryExtensions
|
||||
{
|
||||
public static void Record(this IActionsHistory history, Action execute, Action unexecute, string? label = default)
|
||||
=> history.Record(new DelegateAction(execute, unexecute, label));
|
||||
|
||||
public static void ExecuteAction(this IActionsHistory history, IAction action)
|
||||
{
|
||||
history.Record(action);
|
||||
action.Execute();
|
||||
}
|
||||
}
|
||||
|
||||
public class ActionsHistory : IActionsHistory
|
||||
{
|
||||
private readonly List<IAction> _history = new List<IAction>();
|
||||
private readonly List<IAction> _batchHistory = new List<IAction>();
|
||||
private int _position = -1;
|
||||
private bool _isApplyingOperation = false;
|
||||
private string? _batchLabel;
|
||||
private int _batchDepth;
|
||||
|
||||
private static readonly PropertyChangedEventArgs _canRedoArgs = new PropertyChangedEventArgs(nameof(CanRedo));
|
||||
private static readonly PropertyChangedEventArgs _canUndoArgs = new PropertyChangedEventArgs(nameof(CanUndo));
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
public static readonly ActionsHistory Global = new ActionsHistory();
|
||||
|
||||
public bool IsBatching { get; private set; }
|
||||
|
||||
public int MaxSize { get; set; } = 50;
|
||||
|
||||
public bool CanRedo => _history.Count > 0 && _position < _history.Count - 1;
|
||||
|
||||
public bool CanUndo => _position > -1;
|
||||
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
public IAction? Current => CanUndo ? _history[_position] : null;
|
||||
|
||||
public IDisposable Batch(string? label = default)
|
||||
=> new BatchOperation(label, this);
|
||||
|
||||
public void Record(IAction op)
|
||||
{
|
||||
// Prevent recording the undo or redo operation
|
||||
if (_isApplyingOperation || !IsEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsBatching)
|
||||
{
|
||||
_batchHistory.Add(op);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddToUndoStack(op);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddToUndoStack(IAction op)
|
||||
{
|
||||
if (_position < _history.Count - 1)
|
||||
{
|
||||
_history.RemoveRange(_position + 1, _history.Count - _position - 1);
|
||||
}
|
||||
|
||||
if (_history.Count >= MaxSize)
|
||||
{
|
||||
_history.RemoveAt(0);
|
||||
_position--;
|
||||
}
|
||||
|
||||
_history.Add(op);
|
||||
_position++;
|
||||
|
||||
PropertyChanged?.Invoke(this, _canRedoArgs);
|
||||
PropertyChanged?.Invoke(this, _canUndoArgs);
|
||||
}
|
||||
|
||||
public void Undo()
|
||||
{
|
||||
if (IsBatching)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(Undo)} is not allowed during a batch.");
|
||||
}
|
||||
|
||||
if (CanUndo)
|
||||
{
|
||||
var op = _history[_position];
|
||||
_isApplyingOperation = true;
|
||||
op.Undo();
|
||||
_isApplyingOperation = false;
|
||||
_position--;
|
||||
}
|
||||
}
|
||||
|
||||
public void Redo()
|
||||
{
|
||||
if (IsBatching)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(Redo)} is not allowed during a batch.");
|
||||
}
|
||||
|
||||
if (CanRedo)
|
||||
{
|
||||
_position++;
|
||||
var op = _history[_position];
|
||||
_isApplyingOperation = true;
|
||||
op.Execute();
|
||||
_isApplyingOperation = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_history.Clear();
|
||||
_batchHistory.Clear();
|
||||
}
|
||||
|
||||
public void Pause(string? label = default)
|
||||
{
|
||||
if (_batchDepth > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_batchLabel = label;
|
||||
IsBatching = true;
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
if (_batchDepth > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_batchHistory.Count > 0)
|
||||
{
|
||||
AddToUndoStack(new BatchAction(_batchLabel, _batchHistory));
|
||||
_batchHistory.Clear();
|
||||
}
|
||||
|
||||
_batchLabel = null;
|
||||
IsBatching = false;
|
||||
}
|
||||
|
||||
private class BatchOperation : IDisposable
|
||||
{
|
||||
private readonly ActionsHistory _history;
|
||||
private bool _disposed;
|
||||
|
||||
public BatchOperation(string? label, ActionsHistory history)
|
||||
{
|
||||
_history = history;
|
||||
_history.Pause(label);
|
||||
_history._batchDepth++;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
_history._batchDepth--;
|
||||
_history.Resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Examples/Nodify.Shared/UndoRedo/BatchAction.cs
Normal file
37
Examples/Nodify.Shared/UndoRedo/BatchAction.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Nodify.UndoRedo
|
||||
{
|
||||
public class BatchAction : IAction
|
||||
{
|
||||
public BatchAction(string? label, IEnumerable<IAction> history)
|
||||
{
|
||||
History = history.Reverse().ToList();
|
||||
Label = label;
|
||||
}
|
||||
|
||||
public IReadOnlyList<IAction> History { get; }
|
||||
|
||||
public string? Label { get; }
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
for (int i = History.Count - 1; i >= 0; i--)
|
||||
{
|
||||
History[i].Execute();
|
||||
}
|
||||
}
|
||||
|
||||
public void Undo()
|
||||
{
|
||||
for (int i = 0; i < History.Count; i++)
|
||||
{
|
||||
History[i].Undo();
|
||||
}
|
||||
}
|
||||
|
||||
public override string? ToString()
|
||||
=> Label;
|
||||
}
|
||||
}
|
||||
25
Examples/Nodify.Shared/UndoRedo/DelegateAction.cs
Normal file
25
Examples/Nodify.Shared/UndoRedo/DelegateAction.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace Nodify.UndoRedo
|
||||
{
|
||||
public class DelegateAction : IAction
|
||||
{
|
||||
private readonly Action _execute;
|
||||
private readonly Action _undo;
|
||||
|
||||
public string? Label { get; }
|
||||
|
||||
public DelegateAction(Action apply, Action unapply, string? label)
|
||||
{
|
||||
_execute = apply;
|
||||
_undo = unapply;
|
||||
Label = label;
|
||||
}
|
||||
|
||||
public void Execute() => _execute();
|
||||
public void Undo() => _undo();
|
||||
|
||||
public override string? ToString()
|
||||
=> Label;
|
||||
}
|
||||
}
|
||||
84
Examples/Nodify.Shared/UndoRedo/PropertyCache.cs
Normal file
84
Examples/Nodify.Shared/UndoRedo/PropertyCache.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Nodify.UndoRedo
|
||||
{
|
||||
public interface IPropertyAccessor
|
||||
{
|
||||
object? GetValue(object instance);
|
||||
void SetValue(object instance, object? value);
|
||||
bool CanRead { get; }
|
||||
bool CanWrite { get; }
|
||||
}
|
||||
|
||||
public sealed class PropertyAccessor<TInstanceType, TPropertyType> : IPropertyAccessor where TInstanceType : class
|
||||
{
|
||||
private readonly Func<TInstanceType, TPropertyType> _getter;
|
||||
private readonly Action<TInstanceType, TPropertyType> _setter;
|
||||
|
||||
public bool CanRead { get; }
|
||||
public bool CanWrite { get; }
|
||||
|
||||
public PropertyAccessor(Func<TInstanceType, TPropertyType> getter, Action<TInstanceType, TPropertyType> setter)
|
||||
{
|
||||
_getter = getter;
|
||||
_setter = setter;
|
||||
|
||||
CanRead = getter != null;
|
||||
CanWrite = setter != null;
|
||||
}
|
||||
|
||||
public object? GetValue(object instance)
|
||||
=> _getter((TInstanceType)instance);
|
||||
|
||||
public void SetValue(object instance, object? value)
|
||||
=> _setter((TInstanceType)instance, (TPropertyType)value!);
|
||||
}
|
||||
|
||||
public class PropertyCache
|
||||
{
|
||||
private static readonly Dictionary<string, IPropertyAccessor> _properties = new Dictionary<string, IPropertyAccessor>();
|
||||
|
||||
public static IPropertyAccessor Get(Type type, string name)
|
||||
{
|
||||
string propKey = $"{type.FullName}.{name}";
|
||||
if (!_properties.TryGetValue(propKey, out var result))
|
||||
{
|
||||
var prop = type.GetProperty(name);
|
||||
result = Create(type, prop!);
|
||||
|
||||
_properties.Add(propKey, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static IPropertyAccessor Get<T>(string name)
|
||||
=> Get(typeof(T), name);
|
||||
|
||||
private static IPropertyAccessor Create(Type type, PropertyInfo property)
|
||||
{
|
||||
Delegate? getterInvocation = default;
|
||||
Delegate? setterInvocation = default;
|
||||
|
||||
if (property.CanRead)
|
||||
{
|
||||
MethodInfo getMethod = property.GetGetMethod(true)!;
|
||||
Type getterType = typeof(Func<,>).MakeGenericType(type, property.PropertyType);
|
||||
getterInvocation = Delegate.CreateDelegate(getterType, getMethod);
|
||||
}
|
||||
|
||||
if (property.CanWrite)
|
||||
{
|
||||
MethodInfo setMethod = property.GetSetMethod(true)!;
|
||||
Type setterType = typeof(Action<,>).MakeGenericType(type, property.PropertyType);
|
||||
setterInvocation = Delegate.CreateDelegate(setterType, setMethod);
|
||||
}
|
||||
|
||||
Type adapterType = typeof(PropertyAccessor<,>).MakeGenericType(type, property.PropertyType);
|
||||
|
||||
return (IPropertyAccessor)Activator.CreateInstance(adapterType, getterInvocation, setterInvocation)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
80
Examples/Nodify.Shared/UndoRedo/Undoable.cs
Normal file
80
Examples/Nodify.Shared/UndoRedo/Undoable.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Expression = System.Linq.Expressions.Expression;
|
||||
|
||||
namespace Nodify.UndoRedo
|
||||
{
|
||||
[Flags]
|
||||
public enum PropertyFlags
|
||||
{
|
||||
Disable = 0,
|
||||
Enable = 1
|
||||
}
|
||||
|
||||
public abstract class Undoable : ObservableObject
|
||||
{
|
||||
private readonly HashSet<string> _trackedProperties = new HashSet<string>();
|
||||
|
||||
public IActionsHistory History { get; }
|
||||
|
||||
private void RecordHistory<TPropType>(string propName, TPropType previous, TPropType current)
|
||||
{
|
||||
if (_trackedProperties.Contains(propName))
|
||||
{
|
||||
var prop = PropertyCache.Get(GetType(), propName);
|
||||
History.Record(() => prop.SetValue(this, current), () => prop.SetValue(this, previous), propName);
|
||||
}
|
||||
}
|
||||
|
||||
protected void RecordProperty(string propName, PropertyFlags flags = PropertyFlags.Enable)
|
||||
{
|
||||
if (flags == PropertyFlags.Disable)
|
||||
{
|
||||
_trackedProperties.Remove(propName);
|
||||
}
|
||||
else if (flags.HasFlag(PropertyFlags.Enable))
|
||||
{
|
||||
_trackedProperties.Add(propName);
|
||||
}
|
||||
}
|
||||
|
||||
protected void RecordProperty<TType>(Expression<Func<TType, object?>> selector, PropertyFlags flags = PropertyFlags.Enable)
|
||||
{
|
||||
string name = GetPropertyName(selector);
|
||||
RecordProperty(name, flags);
|
||||
}
|
||||
|
||||
private static string GetPropertyName(Expression memberAccess)
|
||||
=> memberAccess switch
|
||||
{
|
||||
LambdaExpression lambda => GetPropertyName(lambda.Body),
|
||||
MemberExpression mbr => mbr.Member.Name,
|
||||
UnaryExpression unary => GetPropertyName(unary.Operand),
|
||||
_ => throw new Exception($"Member name could not be extracted from {memberAccess}.")
|
||||
};
|
||||
|
||||
protected override bool SetProperty<TPropType>(ref TPropType field, TPropType value, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
TPropType prev = field;
|
||||
if (base.SetProperty(ref field, value, propertyName))
|
||||
{
|
||||
RecordHistory(propertyName, prev, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Undoable()
|
||||
{
|
||||
History = ActionsHistory.Global;
|
||||
}
|
||||
|
||||
public Undoable(IActionsHistory history)
|
||||
{
|
||||
History = history;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user