Skip to content

Commit 72ef349

Browse files
committed
Edit tracking + undo redo for primitive properties
Working on class/struct properties next. Don't want to resort to making every complex field a full EditorObject like I did in C++. That created some serious bloat in the DB text file and could add unecessary overhead if editor objects get heavier in the future.
1 parent 28a52cf commit 72ef349

File tree

7 files changed

+427
-92
lines changed

7 files changed

+427
-92
lines changed

src/App/Project/DiffUtil.bf

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using System.Collections;
2+
using System.Reflection;
3+
using Nanoforge;
4+
using System;
5+
6+
namespace Nanoforge.App.Project
7+
{
8+
static
9+
{
10+
public static mixin BeginCommit(StringView commitName)
11+
{
12+
DiffUtil diffUtil = scope:: .(commitName);
13+
diffUtil
14+
}
15+
16+
//Override for when you're only tracking changes on one object
17+
public static mixin BeginCommit<T>(StringView commitName, T obj) where T : EditorObject
18+
{
19+
DiffUtil diffUtil = scope:: .(commitName);
20+
diffUtil.Track<T>(obj);
21+
diffUtil
22+
}
23+
}
24+
25+
//Tracks changes made to editor objects, generates a set of transactions representing those changes, and commits them to the project DB.
26+
//Transactions can be rolled back prior to committing them. Useful if an error occurs.
27+
//DiffUtil is NOT thread safe. You should only use an instance with one thread at a time.
28+
public class DiffUtil : IDisposable
29+
{
30+
private Dictionary<EditorObject, ISnapshot> _snapshots = new .() ~DeleteDictionaryAndValues!(_);
31+
private List<EditorObject> _newObjects = new .() ~delete _;
32+
private Dictionary<EditorObject, String> _newObjectNames = new .() ~DeleteDictionaryAndValues!(_);
33+
public readonly String CommitName = new .() ~delete _;
34+
public bool Cancelled { get; private set; } = false;
35+
36+
public this(StringView commitName)
37+
{
38+
CommitName.Set(commitName);
39+
}
40+
41+
public void Track<T>(T target) where T : EditorObject
42+
{
43+
_snapshots[target] = new Snapshot<T>(target);
44+
}
45+
46+
public void Commit()
47+
{
48+
//Generate property change transactions
49+
List<ITransaction> transactions = scope .();
50+
for (var kv in _snapshots)
51+
{
52+
EditorObject target = kv.key;
53+
var snapshot = kv.value;
54+
55+
//For new objects generate a creation transaction + add the object to the DB
56+
if (_newObjects.Contains(target))
57+
{
58+
transactions.Add(new CreateObjectTransaction(target, _newObjectNames.GetValueOrDefault(target)));
59+
ProjectDB.AddObject(target);
60+
if (_newObjectNames.ContainsKey(target))
61+
ProjectDB.SetObjectName(target, _newObjectNames[target]);
62+
}
63+
64+
//Generate transactions describing property changes
65+
snapshot.GenerateDiffTransactions(transactions);
66+
}
67+
68+
ProjectDB.Commit(transactions, CommitName);
69+
}
70+
71+
public void Rollback()
72+
{
73+
Cancelled = true;
74+
75+
//Delete new objects + their names
76+
for (EditorObject obj in _newObjects)
77+
delete obj;
78+
79+
DeleteDictionaryAndValues!(_newObjectNames);
80+
81+
//Apply snapshots to pre-existing objects to undo changes
82+
for (var kv in _snapshots)
83+
{
84+
EditorObject obj = kv.key;
85+
ISnapshot snapshot = kv.value;
86+
snapshot.Apply(obj);
87+
}
88+
}
89+
90+
public void Cancel()
91+
{
92+
Cancelled = true;
93+
}
94+
95+
public void Dispose()
96+
{
97+
if (Cancelled)
98+
{
99+
Rollback();
100+
}
101+
else
102+
{
103+
Commit();
104+
}
105+
}
106+
107+
public T CreateObject<T>(StringView name) where T : EditorObject
108+
{
109+
T obj = new T();
110+
_newObjects.Add(obj);
111+
if (name != "")
112+
_newObjectNames[obj] = new .()..Set(name);
113+
114+
Track<T>(obj);
115+
return obj;
116+
}
117+
}
118+
}

src/App/Project/EditorObject.bf

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,8 @@ namespace Nanoforge.App
1414

1515
public class EditorObject
1616
{
17-
public String Name
18-
{
19-
get
20-
{
21-
return ProjectDB.GetObjectName(this);
22-
}
23-
set
24-
{
25-
ProjectDB.SetObjectName(this, value);
26-
}
27-
}
17+
public bool HasName => ProjectDB.GetObjectName(this) case .Ok;
18+
public Result<String> GetName() => ProjectDB.GetObjectName(this);
19+
public void SetName(StringView name) => ProjectDB.SetObjectName(this, name);
2820
}
2921
}

src/App/Project/Helpers.bf

Lines changed: 1 addition & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,9 @@
11
using System.Collections;
2-
using System.Reflection;
32
using Nanoforge;
43
using System;
54

65
namespace Nanoforge.App.Project
76
{
8-
static
9-
{
10-
public static DiffUtil<T> TrackChanges<T>(T obj, StringView commitName) where T : EditorObject
11-
{
12-
DiffUtil<T> self = new .(obj, commitName);
13-
self.CreateSnapshot(obj);
14-
return self;
15-
}
16-
}
17-
187
public struct EditorPropertyInfo
198
{
209
public StringView Name;
@@ -33,20 +22,10 @@ namespace Nanoforge.App.Project
3322
}
3423
}
3524

36-
public class DiffUtil<T> : IDisposable
37-
where T : EditorObject
25+
public struct TypeUtil<T> where T : EditorObject
3826
{
39-
public T Target;
40-
public readonly String CommitName = new .() ~delete _;
41-
4227
public const EditorPropertyInfo[?] PropertyInfo = GetEditorProperties();
4328

44-
public this(T target, StringView commitName)
45-
{
46-
Target = target;
47-
CommitName.Set(commitName);
48-
}
49-
5029
//Grab a list of tracked properties and their metadata during comptime for quick operations at runtime
5130
[Comptime(ConstEval=true)]
5231
private static Span<EditorPropertyInfo> GetEditorProperties()
@@ -65,54 +44,5 @@ namespace Nanoforge.App.Project
6544
}
6645
return properties;
6746
}
68-
69-
[OnCompile(.TypeInit), Comptime]
70-
public static void GenerateCode()
71-
{
72-
//Generate fields for holding snapshot of the object state
73-
Type selfType = typeof(Self);
74-
for (var field in typeof(T).GetFields())
75-
{
76-
//Only track fields with [EditorProperty] attribute
77-
var result = field.GetCustomAttribute<EditorPropertyAttribute>();
78-
if (result == .Err)
79-
continue;
80-
81-
StringView fieldName = field.Name;
82-
var fieldTypeName = field.FieldType.GetFullName(.. scope .());
83-
Compiler.EmitTypeBody(selfType, scope $"private {fieldTypeName} {fieldName};\n");
84-
}
85-
86-
//Generate CreateSnapshot()
87-
Compiler.EmitTypeBody(selfType, scope $"\npublic void CreateSnapshot({typeof(T).GetFullName(.. scope .())} obj)");
88-
Compiler.EmitTypeBody(selfType, "\n{\n");
89-
int offset = 0;
90-
for (var field in typeof(T).GetFields())
91-
{
92-
//Only track fields with [EditorProperty] attribute
93-
var result = field.GetCustomAttribute<EditorPropertyAttribute>();
94-
if (result == .Err)
95-
continue;
96-
97-
StringView fieldName = field.Name;
98-
Compiler.EmitTypeBody(selfType, scope $" this.{fieldName} = obj.[Friend]{fieldName};\n");
99-
offset += field.FieldType.InstanceSize;
100-
}
101-
Compiler.EmitTypeBody(selfType, "}");
102-
103-
//TODO: Generate Commit()
104-
}
105-
106-
public void Commit()
107-
{
108-
//TODO: Iterate through properties and compare snapshot value with current value
109-
//TODO: Generate transactions for different properties
110-
}
111-
112-
public void Dispose()
113-
{
114-
Commit();
115-
delete this;
116-
}
11747
}
11848
}

src/App/Project/ProjectDB.bf

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Nanoforge.App.Project;
12
using System.Diagnostics;
23
using System.Collections;
34
using System.Threading;
@@ -23,25 +24,49 @@ namespace Nanoforge.App
2324
static Dictionary<EditorObject, String> _objectNames = new .() ~DeleteDictionaryAndValues!(_); //Names owned by Project so we don't add unnecessary bloat to unnamed objects
2425
static append Monitor _objectNameDictionaryLock;
2526
static append Monitor _objectCreationLock;
27+
static append Monitor _commitLock;
2628

27-
public static T CreateObject<T>() where T : EditorObject, new
29+
//Undo/redo stacks. Just using plain lists + Add() & PopBack() to handle pushing onto the stack and popping off it.
30+
//Eventually should replace with a Stack<T> class that inherits List<T> and hides non stack function. At the moment Beef doesn't have a standard stack class.
31+
public static List<Commit> UndoStack = new .() ~DeleteContainerAndItems!(_);
32+
public static List<Commit> RedoStack = new .() ~DeleteContainerAndItems!(_);
33+
34+
public static T CreateObject<T>(StringView name = "") where T : EditorObject, new
2835
{
2936
ScopedLock!(_objectCreationLock);
3037
T obj = new T();
3138
_objects.Add(obj);
39+
if (name != "")
40+
SetObjectName(obj, name);
3241
return obj;
3342
}
3443

35-
public static String GetObjectName(EditorObject object)
44+
//Used to add objects created by DiffUtil when it commits changes. It creates its own objects so they can be destroyed on rollback and don't exist program wide until commit.
45+
public static void AddObject(EditorObject obj)
46+
{
47+
ScopedLock!(_objectCreationLock);
48+
_objects.Add(obj);
49+
}
50+
51+
//Removes object from ProjectDB. Not recommended for direct use. Doesn't delete the object. That way transactions can still hold object info on the redo stack to restore it as required
52+
//Currently doesn't remove any references to this object that others might have. If used in the undo/redo stack that shouldn't frequently be a problem.
53+
public static void RemoveObject(EditorObject obj)
54+
{
55+
ScopedLock!(_objectCreationLock);
56+
_objects.Remove(obj);
57+
_objectNames.Remove(obj);
58+
}
59+
60+
public static Result<String> GetObjectName(EditorObject object)
3661
{
3762
ScopedLock!(_objectNameDictionaryLock);
3863
if (_objectNames.ContainsKey(object))
3964
{
40-
return _objectNames[object];
65+
return .Ok(_objectNames[object]);
4166
}
4267
else
4368
{
44-
return null;
69+
return .Err;
4570
}
4671
}
4772

@@ -58,7 +83,7 @@ namespace Nanoforge.App
5883
}
5984
}
6085

61-
public static EditorObject GetObjectByName(StringView name)
86+
public static EditorObject Find(StringView name)
6287
{
6388
for (var kv in _objectNames)
6489
if (StringView.Equals(kv.value, name))
@@ -67,10 +92,10 @@ namespace Nanoforge.App
6792
return null;
6893
}
6994

70-
public static T GetObjectByName<T>(StringView name) where T : EditorObject
95+
public static T Find<T>(StringView name) where T : EditorObject
7196
{
72-
Object obj = GetObjectByName(name);
73-
return obj.GetType() == typeof(T) ? (T)obj : null;
97+
Object obj = Find(name);
98+
return (obj != null && obj.GetType() == typeof(T)) ? (T)obj : null;
7499
}
75100

76101
public static void Reset()
@@ -86,5 +111,42 @@ namespace Nanoforge.App
86111
_objectNames.Clear();
87112
Ready = false;
88113
}
114+
115+
//Commit changes to undo stack. ProjectDB takes ownership of the transactions
116+
public static void Commit(Span<ITransaction> transactions, StringView commitName)
117+
{
118+
ScopedLock!(_commitLock);
119+
UndoStack.Add(new Commit(commitName, transactions));
120+
}
121+
122+
//Undo single commit
123+
public static void Undo()
124+
{
125+
ScopedLock!(_commitLock);
126+
if (UndoStack.Count > 0)
127+
{
128+
Commit undo = UndoStack.PopBack();
129+
for (ITransaction transaction in undo.Transactions)
130+
{
131+
transaction.Revert();
132+
}
133+
RedoStack.Add(undo);
134+
}
135+
}
136+
137+
//Redo single commit
138+
public static void Redo()
139+
{
140+
ScopedLock!(_commitLock);
141+
if (RedoStack.Count > 0)
142+
{
143+
Commit redo = RedoStack.PopBack();
144+
for (ITransaction transaction in redo.Transactions)
145+
{
146+
transaction.Apply();
147+
}
148+
UndoStack.Add(redo);
149+
}
150+
}
89151
}
90152
}

0 commit comments

Comments
 (0)