Profile editor - Fix auto-saving, failing commands, suspended editing and smaller editor issues

- A failed auto-save no longer stops auto-saving for the rest of the session
- A failing command is no longer also reported to ReactiveUI as unhandled, on top of the error dialog
- Resuming editing applies the editor's time again right away
- Nothing is focused while editing is suspended
- A layer created on another layer gets a name that isn't in use next to it
- CanUndo and CanRedo start out false, so new subscribers know the state right away
- An empty command scope doesn't add an undo step
- New subscribers to History get the current history once
- Expanding the keyframe selection with an already selected keyframe doesn't add it twice
- Snapping can snap to a snap time of zero
This commit is contained in:
RobertBeekman committed 2026-10-20 09:47:43 +02:00
1 parent 0920cf8917
commit 55dc04163c
8 files changed
+60 -62

No files matched your search

@@ -14,8 +14,8 @@ namespace Artemis.UI.Shared.Services.ProfileEditor;
/// </summary>
public class ProfileEditorHistory
{
private readonly Subject<bool> _canRedo = new();
private readonly Subject<bool> _canUndo = new();
private readonly BehaviorSubject<bool> _canRedo = new(false);
private readonly BehaviorSubject<bool> _canUndo = new(false);
private readonly Stack<IProfileEditorCommand> _redoCommands = new();
private readonly Stack<IProfileEditorCommand> _undoCommands = new();
@@ -28,6 +28,9 @@ public class ProfileEditorHistory
ProfileConfiguration = profileConfiguration;
Execute = ReactiveCommand.Create<IProfileEditorCommand>(ExecuteEditorCommand);
// Whoever executes a command reports its failure (the profile editor service shows a dialog), without this
// ReactiveUI would also treat it as unhandled
Execute.ThrownExceptions.Subscribe(_ => { });
Undo = ReactiveCommand.Create(ExecuteUndo, CanUndo);
Redo = ReactiveCommand.Create(ExecuteRedo, CanRedo);
}
@@ -56,7 +56,7 @@ internal class ProfileEditorService : IProfileEditorService
ProfileConfiguration = _profileConfigurationSubject.AsObservable();
ProfileElement = _profileElementSubject.AsObservable();
LayerProperty = _layerPropertySubject.AsObservable();
History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory));
History = ProfileConfiguration.Select(GetHistory);
Time = _timeSubject.AsObservable();
Playing = _playingSubject.AsObservable();
SuspendedEditing = _suspendedEditingSubject.AsObservable();
@@ -101,9 +101,9 @@ internal class ProfileEditorService : IProfileEditorService
}
catch (Exception e)
{
// Don't rethrow, that would end the observable that drives auto-saving
_windowService.ShowExceptionDialog("Failed to auto-save profile", e);
_logger.Error(e, "Failed to auto-save profile");
throw;
}
return Unit.Default;
@@ -138,8 +138,12 @@ internal class ProfileEditorService : IProfileEditorService
private void ApplyFocusMode()
{
// While editing is suspended the profile plays normally, without focus
if (_suspendedEditingSubject.Value)
{
_profileService.FocusProfileElement = null;
return;
}
_profileService.FocusProfileElement = _focusModeSubject.Value switch
{
@@ -235,10 +239,12 @@ internal class ProfileEditorService : IProfileEditorService
{
if (_profileConfigurationSubject.Value != null)
_profileService.UpdateFocusProfile = false;
Tick(_timeSubject.Value);
}
_suspendedEditingSubject.OnNext(suspend);
// Apply the editor's time again, this only works once editing is no longer suspended
if (!suspend)
Tick(_timeSubject.Value);
ApplyFocusMode();
}
@@ -271,7 +277,10 @@ internal class ProfileEditorService : IProfileEditorService
else
{
if (expand)
_selectedKeyframes.Add(keyframe);
{
if (!_selectedKeyframes.Items.Contains(keyframe))
_selectedKeyframes.Add(keyframe);
}
else
_selectedKeyframes.Edit(l =>
{
@@ -324,9 +333,11 @@ internal class ProfileEditorService : IProfileEditorService
if (snapTimes != null)
{
// Find the closest keyframe
TimeSpan closeSnapTime = snapTimes.FirstOrDefault(s => Math.Abs(time.TotalMilliseconds - s.TotalMilliseconds) < tolerance.TotalMilliseconds)!;
if (closeSnapTime != TimeSpan.Zero)
return closeSnapTime;
TimeSpan? closeSnapTime = snapTimes.Where(s => Math.Abs(time.TotalMilliseconds - s.TotalMilliseconds) < tolerance.TotalMilliseconds)
.Cast<TimeSpan?>()
.FirstOrDefault();
if (closeSnapTime != null)
return closeSnapTime.Value;
}
return time;
@@ -368,7 +379,7 @@ internal class ProfileEditorService : IProfileEditorService
{
if (target is Layer targetLayer)
{
Layer layer = new(targetLayer.Parent, targetLayer.GetNewLayerName());
Layer layer = new(targetLayer.Parent, targetLayer.Parent.GetNewLayerName());
_layerBrushService.ApplyDefaultBrush(layer);
layer.AddLeds(_deviceService.EnabledDevices.SelectMany(d => d.Leds));
@@ -463,6 +474,10 @@ internal class ProfileEditorService : IProfileEditorService
ProfileEditorCommandScope scope = _profileEditorHistoryScope;
_profileEditorHistoryScope = null;
// Don't add an undo step that does nothing
if (!scope.ProfileEditorCommands.Any())
return;
// Executing the composite command won't do anything the first time (see last ctor variable)
// commands were already executed each time they were added to the scope
ExecuteCommand(new CompositeCommand(scope.ProfileEditorCommands, scope.Name, true));