Skip to content

Commit 9330c75

Browse files
authored
Merge pull request stride3d#2759 from Color-Rise/feature/xplat/action-history
feat: undo/redo & action history panel
2 parents e68c292 + 2353649 commit 9330c75

File tree

22 files changed

+466
-139
lines changed

22 files changed

+466
-139
lines changed

sources/assets/Stride.Core.Assets.Quantum/IAssetObjectNode.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ public interface IAssetObjectNode : IAssetNode, IObjectNode
2020

2121
bool IsItemDeleted(ItemId itemId);
2222

23-
void Restore(object restoredItem, ItemId id);
23+
void Restore(object? restoredItem, ItemId id);
2424

25-
void Restore(object restoredItem, NodeIndex index, ItemId id);
25+
void Restore(object? restoredItem, NodeIndex index, ItemId id);
2626

27-
void RemoveAndDiscard(object item, NodeIndex itemIndex, ItemId id);
27+
void RemoveAndDiscard(object? item, NodeIndex itemIndex, ItemId id);
2828

2929
bool IsItemInherited(NodeIndex index);
3030

sources/assets/Stride.Core.Assets.Quantum/Internal/AssetBoxedNode.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ public AssetBoxedNode(INodeBuilder nodeBuilder, object value, Guid guid, ITypeDe
4343

4444
public bool IsItemDeleted(ItemId itemId) => ex.IsItemDeleted(itemId);
4545

46-
public void Restore(object restoredItem, ItemId id) => ex.Restore(restoredItem, id);
46+
public void Restore(object? restoredItem, ItemId id) => ex.Restore(restoredItem, id);
4747

48-
public void Restore(object restoredItem, NodeIndex index, ItemId id) => ex.Restore(restoredItem, index, id);
48+
public void Restore(object? restoredItem, NodeIndex index, ItemId id) => ex.Restore(restoredItem, index, id);
4949

50-
public void RemoveAndDiscard(object item, NodeIndex itemIndex, ItemId id) => ex.RemoveAndDiscard(item, itemIndex, id);
50+
public void RemoveAndDiscard(object? item, NodeIndex itemIndex, ItemId id) => ex.RemoveAndDiscard(item, itemIndex, id);
5151

5252
public OverrideType GetItemOverride(NodeIndex index) => ex.GetItemOverride(index);
5353

sources/assets/Stride.Core.Assets.Quantum/Internal/AssetObjectNode.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ public AssetObjectNode(INodeBuilder nodeBuilder, object value, Guid guid, ITypeD
4444

4545
public bool IsItemDeleted(ItemId itemId) => ex.IsItemDeleted(itemId);
4646

47-
public void Restore(object restoredItem, ItemId id) => ex.Restore(restoredItem, id);
47+
public void Restore(object? restoredItem, ItemId id) => ex.Restore(restoredItem, id);
4848

49-
public void Restore(object restoredItem, NodeIndex index, ItemId id) => ex.Restore(restoredItem, index, id);
49+
public void Restore(object? restoredItem, NodeIndex index, ItemId id) => ex.Restore(restoredItem, index, id);
5050

51-
public void RemoveAndDiscard(object item, NodeIndex itemIndex, ItemId id) => ex.RemoveAndDiscard(item, itemIndex, id);
51+
public void RemoveAndDiscard(object? item, NodeIndex itemIndex, ItemId id) => ex.RemoveAndDiscard(item, itemIndex, id);
5252

5353
public OverrideType GetItemOverride(NodeIndex index) => ex.GetItemOverride(index);
5454

sources/assets/Stride.Core.Assets.Quantum/Internal/AssetObjectNodeExtended.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ private bool TryGetCollectionItemIds(object? instance, [MaybeNullWhen(false)] ou
120120
return result;
121121
}
122122

123-
public void Restore(object restoredItem, ItemId id)
123+
public void Restore(object? restoredItem, ItemId id)
124124
{
125125
if (TryGetCollectionItemIds(node.Retrieve(), out var ids))
126126
{
@@ -131,7 +131,7 @@ public void Restore(object restoredItem, ItemId id)
131131
node.Add(restoredItem);
132132
}
133133

134-
public void Restore(object restoredItem, NodeIndex index, ItemId id)
134+
public void Restore(object? restoredItem, NodeIndex index, ItemId id)
135135
{
136136
restoringId = id;
137137
node.Add(restoredItem, index);
@@ -143,7 +143,7 @@ public void Restore(object restoredItem, NodeIndex index, ItemId id)
143143
}
144144
}
145145

146-
public void RemoveAndDiscard(object item, NodeIndex itemIndex, ItemId id)
146+
public void RemoveAndDiscard(object? item, NodeIndex itemIndex, ItemId id)
147147
{
148148
node.Remove(item, itemIndex);
149149
if (TryGetCollectionItemIds(node.Retrieve(), out var ids))

sources/core/Stride.Core.Reflection/TypeDescriptors/ArrayDescriptor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public Array CreateArray(int dimension)
5151
return ((Array)array).GetValue(index);
5252
}
5353

54-
public void SetValue(object array, int index, object value)
54+
public void SetValue(object array, int index, object? value)
5555
{
5656
((Array)array).SetValue(value, index);
5757
}

sources/editor/Stride.Core.Assets.Editor.Avalonia/Views/ImageResources.axaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
33
<!-- TODO have different values per theme variant -->
44

5+
<!-- Generic action-related icons -->
6+
<DrawingImage x:Key="ImageSave">
7+
<DrawingImage.Drawing>
8+
<DrawingGroup>
9+
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
10+
<GeometryDrawing Brush="#FFF6F6F6"
11+
Geometry="F1M16,2L16,16 2.586,16 0,13.414 0,2C0,0.897,0.897,0,2,0L14,0C15.103,0,16,0.897,16,2" />
12+
<GeometryDrawing Brush="#FFEFEFF0" Geometry="F1M4,10L4,15 6,15 6,12 8,12 8,15 12,15 12,10z M13,7L3,7 3,3 13,3z" />
13+
<GeometryDrawing Brush="#FF00529C"
14+
Geometry="F1M13,3L3,3 3,7 13,7z M15,2L15,15 12,15 12,10 4,10 4,15 3,15 1,13 1,2C1,1.448,1.448,1,2,1L14,1C14.553,1,15,1.448,15,2 M6,12L8,12 8,15 6,15z" />
15+
</DrawingGroup>
16+
</DrawingImage.Drawing>
17+
</DrawingImage>
18+
519
<!-- Property edition icons -->
620
<DrawingImage x:Key="ImageAdd">
721
<DrawingImage.Drawing>
@@ -15,6 +29,7 @@
1529
</DrawingGroup>
1630
</DrawingImage.Drawing>
1731
</DrawingImage>
32+
1833
<DrawingImage x:Key="ImageClear">
1934
<DrawingImage.Drawing>
2035
<DrawingGroup>
@@ -30,6 +45,7 @@
3045
</DrawingGroup>
3146
</DrawingImage.Drawing>
3247
</DrawingImage>
48+
3349
<DrawingImage x:Key="ImageCreateInstance">
3450
<DrawingImage.Drawing>
3551
<DrawingGroup>
@@ -62,6 +78,7 @@
6278
</DrawingGroup>
6379
</DrawingImage.Drawing>
6480
</DrawingImage>
81+
6582
<DrawingImage x:Key="ImageDropDown">
6683
<DrawingImage.Drawing>
6784
<DrawingGroup>
@@ -75,6 +92,7 @@
7592
</DrawingGroup>
7693
</DrawingImage.Drawing>
7794
</DrawingImage>
95+
7896
<DrawingImage x:Key="ImageFetchAsset">
7997
<DrawingImage.Drawing>
8098
<DrawingGroup>
@@ -105,6 +123,7 @@
105123
</DrawingGroup>
106124
</DrawingImage.Drawing>
107125
</DrawingImage>
126+
108127
<DrawingImage x:Key="ImagePickup">
109128
<DrawingImage.Drawing>
110129
<DrawingGroup>
@@ -118,6 +137,7 @@
118137
</DrawingGroup>
119138
</DrawingImage.Drawing>
120139
</DrawingImage>
140+
121141
<DrawingImage x:Key="ImageRemove">
122142
<DrawingImage.Drawing>
123143
<DrawingGroup>

sources/editor/Stride.Core.Assets.Editor.Tests/TestArchetypesBasicUndoRedo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
33

44
using System.Linq;
5-
using Stride.Core.Assets.Editor.Quantum;
5+
using Stride.Core.Assets.Presentation.Quantum;
66
using Stride.Core.Assets.Quantum;
77
using Stride.Core.Assets.Quantum.Tests;
88
using Stride.Core.Presentation.Dirtiables;

sources/editor/Stride.Core.Assets.Editor/Components/Properties/SessionObjectPropertiesViewModel.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,19 +142,19 @@ protected override void OnPropertyChanged(params string[] propertyNames)
142142
{
143143
base.OnPropertyChanged(propertyNames);
144144
// Do this only when the property has changed from the UI thread
145-
if (Dispatcher.CheckAccess())
145+
if (!Dispatcher.CheckAccess())
146+
return;
147+
148+
if (!contextLock && propertyNames.Any(x => x is nameof(ViewModel) or nameof(CanDisplayProperties) or nameof(FallbackMessage)))
146149
{
147-
if (!contextLock && propertyNames.Any(x => x is nameof(ViewModel) or nameof(CanDisplayProperties) or nameof(FallbackMessage)))
150+
contextLock = true;
151+
if (Session.ActiveProperties != this)
148152
{
149-
contextLock = true;
150-
if (Session.ActiveProperties != this)
151-
{
152-
// FIXME xplat-editor
153-
//Session.ActiveProperties.ClearSelection();
154-
}
155-
Session.ActiveProperties = this;
156-
contextLock = false;
153+
// FIXME xplat-editor
154+
//Session.ActiveProperties.ClearSelection();
157155
}
156+
Session.ActiveProperties = this;
157+
contextLock = false;
158158
}
159159
}
160160
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
2+
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
3+
4+
using Stride.Core.Assets.Editor.ViewModels;
5+
using Stride.Core.Assets.Presentation.ViewModels;
6+
using Stride.Core.Extensions;
7+
using Stride.Core.Presentation.Collections;
8+
using Stride.Core.Presentation.Commands;
9+
using Stride.Core.Presentation.Dirtiables;
10+
using Stride.Core.Presentation.Services;
11+
using Stride.Core.Presentation.ViewModels;
12+
using Stride.Core.Transactions;
13+
14+
namespace Stride.Core.Assets.Editor.Components.Transactions;
15+
16+
public sealed class ActionHistoryViewModel : DispatcherViewModel
17+
{
18+
private readonly IUndoRedoService service;
19+
private readonly SessionViewModel session;
20+
private readonly ObservableList<TransactionViewModel> transactions = [];
21+
22+
public ActionHistoryViewModel(SessionViewModel session)
23+
: base(session.SafeArgument().ServiceProvider)
24+
{
25+
this.session = session;
26+
service = ServiceProvider.Get<IUndoRedoService>();
27+
transactions.CollectionChanged += (_, _) => RefreshUndoRedoStatus();
28+
UndoCommand = new AnonymousCommand(ServiceProvider, () => { service.Undo(); /*session.CheckConsistency();*/ RefreshUndoRedoStatus(); });
29+
RedoCommand = new AnonymousCommand(ServiceProvider, () => { service.Redo(); /*session.CheckConsistency();*/ RefreshUndoRedoStatus(); });
30+
RefreshUndoRedoStatus();
31+
}
32+
33+
/// <summary>
34+
/// Gets whether it is currently possible to perform an undo operation.
35+
/// </summary>
36+
public bool CanUndo => Transactions.Count > 0 && Transactions.First().IsDone;
37+
38+
/// <summary>
39+
/// Gets whether it is currently possible to perform a redo operation.
40+
/// </summary>
41+
public bool CanRedo => Transactions.Count > 0 && !Transactions.Last().IsDone;
42+
43+
/// <summary>
44+
/// Collection of action view models currently contained in this view model.
45+
/// </summary>
46+
public IReadOnlyObservableCollection<TransactionViewModel> Transactions => transactions;
47+
48+
/// <summary>
49+
/// Command that will perform an undo operation.
50+
/// </summary>
51+
public ICommandBase UndoCommand { get; }
52+
53+
/// <summary>
54+
/// Command that will perform an redo operation.
55+
/// </summary>
56+
public ICommandBase RedoCommand { get; }
57+
58+
/// <summary>
59+
/// Initializes this <see cref="ActionHistoryViewModel"/> and starts to listen to <see cref="IUndoRedoService"/> events.
60+
/// </summary>
61+
public void Initialize()
62+
{
63+
service.Done += TransactionDone;
64+
service.Undone += TransactionUndoneOrRedone;
65+
service.Redone += TransactionUndoneOrRedone;
66+
service.TransactionDiscarded += TransactionDiscarded;
67+
service.Cleared += Cleared;
68+
}
69+
70+
/// <inheritdoc/>
71+
public override void Destroy()
72+
{
73+
service.Done -= TransactionDone;
74+
service.Undone -= TransactionUndoneOrRedone;
75+
service.Redone -= TransactionUndoneOrRedone;
76+
service.TransactionDiscarded -= TransactionDiscarded;
77+
service.Cleared -= Cleared;
78+
base.Destroy();
79+
}
80+
81+
/// <summary>
82+
/// Notify that everything has been saved and create a save point in the action stack.
83+
/// </summary>
84+
internal void NotifySave()
85+
{
86+
service.NotifySave();
87+
transactions.ForEach(x => x.NotifySave());
88+
transactions.ForEach(x => x.IsSavePoint = false);
89+
var savePoint = transactions.LastOrDefault(x => x.IsSaved);
90+
if (savePoint != null)
91+
savePoint.IsSavePoint = true;
92+
}
93+
94+
private void Cleared(object? sender, EventArgs e)
95+
{
96+
Dispatcher.Invoke(() => transactions.Clear());
97+
}
98+
99+
private void TransactionDiscarded(object? sender, TransactionsDiscardedEventArgs e)
100+
{
101+
Dispatcher.Invoke(() => transactions.RemoveWhere(x => e.Transactions.Any(y => x.Id == y.Id)));
102+
}
103+
104+
private async void TransactionDone(object? sender, TransactionEventArgs e)
105+
{
106+
if (e.Transaction.Operations.Count == 0)
107+
return;
108+
109+
var dirtying = e.Transaction.Operations.SelectMany(DirtiableManager.GetDirtyingOperations);
110+
var dirtiables = new HashSet<AssetViewModel>(dirtying.SelectMany(x => x.Dirtiables.OfType<AssetViewModel>()));
111+
if (dirtiables.Count > 0)
112+
{
113+
await session.NotifyAssetPropertiesChangedAsync(dirtiables);
114+
}
115+
await Dispatcher.InvokeAsync(() => transactions.Add(new TransactionViewModel(ServiceProvider, e.Transaction)));
116+
}
117+
118+
private async void TransactionUndoneOrRedone(object? sender, TransactionEventArgs e)
119+
{
120+
var dirtying = e.Transaction.Operations.SelectMany(DirtiableManager.GetDirtyingOperations);
121+
var dirtiables = new HashSet<AssetViewModel>(dirtying.SelectMany(x => x.Dirtiables.OfType<AssetViewModel>()));
122+
if (dirtiables.Count > 0)
123+
{
124+
await session.NotifyAssetPropertiesChangedAsync(dirtiables);
125+
}
126+
127+
await Dispatcher.InvokeAsync(() => transactions.ForEach(x => x.Refresh()));
128+
}
129+
130+
private void RefreshUndoRedoStatus()
131+
{
132+
UndoCommand.IsEnabled = CanUndo;
133+
RedoCommand.IsEnabled = CanRedo;
134+
}
135+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
2+
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
3+
4+
using Stride.Core.Presentation.Dirtiables;
5+
using Stride.Core.Presentation.Services;
6+
using Stride.Core.Presentation.ViewModels;
7+
using Stride.Core.Transactions;
8+
9+
namespace Stride.Core.Assets.Editor.Components.Transactions;
10+
11+
public sealed class TransactionViewModel : DispatcherViewModel
12+
{
13+
private readonly IReadOnlyTransaction transaction;
14+
private bool isDone = true;
15+
private bool isSaved;
16+
private bool isSavePoint;
17+
18+
public TransactionViewModel(IViewModelServiceProvider serviceProvider, IReadOnlyTransaction transaction)
19+
: base(serviceProvider)
20+
{
21+
this.transaction = transaction;
22+
Name = ServiceProvider.Get<IUndoRedoService>().GetName(transaction) ?? string.Empty;
23+
}
24+
25+
/// <summary>
26+
/// Name of this transaction.
27+
/// </summary>
28+
public string Name { get; }
29+
30+
/// <summary>
31+
/// Gets whether this transaction is currently done.
32+
/// </summary>
33+
public bool IsDone { get { return isDone; } private set { SetValue(ref isDone, value); } }
34+
35+
/// <summary>
36+
/// Gets whether this transaction is currently saved.
37+
/// </summary>
38+
public bool IsSaved { get { return isSaved; } private set { SetValue(ref isSaved, value); } }
39+
40+
/// <summary>
41+
/// Get whether this transaction is the current save point.
42+
/// </summary>
43+
public bool IsSavePoint { get { return isSavePoint; } internal set { SetValue(ref isSavePoint, value); } }
44+
45+
/// <summary>
46+
/// Gets the unique identifier of this transaction.
47+
/// </summary>
48+
public Guid Id => transaction.Id;
49+
50+
/// <summary>
51+
/// Refresh the <see cref="IsDone"/> property of this view model.
52+
/// </summary>
53+
internal void Refresh()
54+
{
55+
var dirtying = transaction.Operations.SelectMany(DirtiableManager.GetDirtyingOperations);
56+
IsDone = dirtying.All(x => x.IsDone);
57+
}
58+
59+
/// <summary>
60+
/// Notifies that this transaction has been saved and update <see cref="IsSaved"/> accordingly.
61+
/// </summary>
62+
internal void NotifySave()
63+
{
64+
IsSaved = IsDone;
65+
}
66+
}

0 commit comments

Comments
 (0)