mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Profile editor - Implemented keyframe selection, movement, deletion
Profile editor - Added command scoping
This commit is contained in:
parent
a1f7f6dff8
commit
913117ad0a
@ -213,7 +213,7 @@ namespace Artemis.Core
|
|||||||
SetCurrentValue(CoreJson.DeserializeObject<T>(json)!, null);
|
SetCurrentValue(CoreJson.DeserializeObject<T>(json)!, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReapplyUpdate()
|
internal void ReapplyUpdate()
|
||||||
{
|
{
|
||||||
// Create a timeline with the same position but a delta of zero
|
// Create a timeline with the same position but a delta of zero
|
||||||
Timeline temporaryTimeline = new();
|
Timeline temporaryTimeline = new();
|
||||||
@ -291,6 +291,7 @@ namespace Artemis.Core
|
|||||||
KeyframesEnabled = true;
|
KeyframesEnabled = true;
|
||||||
|
|
||||||
SortKeyframes();
|
SortKeyframes();
|
||||||
|
ReapplyUpdate();
|
||||||
OnKeyframeAdded();
|
OnKeyframeAdded();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,7 +324,9 @@ namespace Artemis.Core
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
_keyframes.Remove(keyframe);
|
_keyframes.Remove(keyframe);
|
||||||
|
|
||||||
SortKeyframes();
|
SortKeyframes();
|
||||||
|
ReapplyUpdate();
|
||||||
OnKeyframeRemoved();
|
OnKeyframeRemoved();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -57,6 +57,7 @@ namespace Artemis.Core
|
|||||||
{
|
{
|
||||||
SetAndNotify(ref _position, value);
|
SetAndNotify(ref _position, value);
|
||||||
LayerProperty.SortKeyframes();
|
LayerProperty.SortKeyframes();
|
||||||
|
LayerProperty.ReapplyUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,21 +2,22 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Services.ProfileEditor;
|
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a profile editor command that can be used to combine multiple commands into one.
|
/// Represents a profile editor command that can be used to combine multiple commands into one.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable
|
public class CompositeCommand : IProfileEditorCommand, IDisposable
|
||||||
{
|
{
|
||||||
|
private bool _ignoreNextExecute;
|
||||||
private readonly List<IProfileEditorCommand> _commands;
|
private readonly List<IProfileEditorCommand> _commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new instance of the <see cref="CompositeProfileEditorCommand" /> class.
|
/// Creates a new instance of the <see cref="CompositeCommand" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="commands">The commands to execute.</param>
|
/// <param name="commands">The commands to execute.</param>
|
||||||
/// <param name="displayName">The display name of the composite command.</param>
|
/// <param name="displayName">The display name of the composite command.</param>
|
||||||
public CompositeProfileEditorCommand(IEnumerable<IProfileEditorCommand> commands, string displayName)
|
public CompositeCommand(IEnumerable<IProfileEditorCommand> commands, string displayName)
|
||||||
{
|
{
|
||||||
if (commands == null)
|
if (commands == null)
|
||||||
throw new ArgumentNullException(nameof(commands));
|
throw new ArgumentNullException(nameof(commands));
|
||||||
@ -24,6 +25,22 @@ public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable
|
|||||||
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
|
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the <see cref="CompositeCommand" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commands">The commands to execute.</param>
|
||||||
|
/// <param name="displayName">The display name of the composite command.</param>
|
||||||
|
/// <param name="ignoreFirstExecute">Whether or not to ignore the first execute because commands are already executed</param>
|
||||||
|
internal CompositeCommand(IEnumerable<IProfileEditorCommand> commands, string displayName, bool ignoreFirstExecute)
|
||||||
|
{
|
||||||
|
if (commands == null)
|
||||||
|
throw new ArgumentNullException(nameof(commands));
|
||||||
|
|
||||||
|
_ignoreNextExecute = ignoreFirstExecute;
|
||||||
|
_commands = commands.ToList();
|
||||||
|
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
@ -40,6 +57,12 @@ public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Execute()
|
public void Execute()
|
||||||
{
|
{
|
||||||
|
if (_ignoreNextExecute)
|
||||||
|
{
|
||||||
|
_ignoreNextExecute = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (IProfileEditorCommand profileEditorCommand in _commands)
|
foreach (IProfileEditorCommand profileEditorCommand in _commands)
|
||||||
profileEditorCommand.Execute();
|
profileEditorCommand.Execute();
|
||||||
}
|
}
|
||||||
@ -48,7 +71,7 @@ public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable
|
|||||||
public void Undo()
|
public void Undo()
|
||||||
{
|
{
|
||||||
// Undo in reverse by iterating from the back
|
// Undo in reverse by iterating from the back
|
||||||
for (int index = _commands.Count; index >= 0; index--)
|
for (int index = _commands.Count - 1; index >= 0; index--)
|
||||||
_commands[index].Undo();
|
_commands[index].Undo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
using Artemis.Core;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a profile editor command that can be used to delete a keyframe.
|
||||||
|
/// </summary>
|
||||||
|
public class DeleteKeyframe<T> : IProfileEditorCommand
|
||||||
|
{
|
||||||
|
private readonly LayerPropertyKeyframe<T> _keyframe;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the <see cref="DeleteKeyframe{T}" /> class.
|
||||||
|
/// </summary>
|
||||||
|
public DeleteKeyframe(LayerPropertyKeyframe<T> keyframe)
|
||||||
|
{
|
||||||
|
_keyframe = keyframe;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Implementation of IProfileEditorCommand
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string DisplayName => "Delete keyframe";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Execute()
|
||||||
|
{
|
||||||
|
_keyframe.LayerProperty.RemoveKeyframe(_keyframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Undo()
|
||||||
|
{
|
||||||
|
_keyframe.LayerProperty.AddKeyframe(_keyframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@ -22,6 +22,16 @@ public class MoveKeyframe : IProfileEditorCommand
|
|||||||
_originalPosition = keyframe.Position;
|
_originalPosition = keyframe.Position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the <see cref="MoveKeyframe" /> class.
|
||||||
|
/// </summary>
|
||||||
|
public MoveKeyframe(ILayerPropertyKeyframe keyframe, TimeSpan position, TimeSpan originalPosition)
|
||||||
|
{
|
||||||
|
_keyframe = keyframe;
|
||||||
|
_position = position;
|
||||||
|
_originalPosition = originalPosition;
|
||||||
|
}
|
||||||
|
|
||||||
#region Implementation of IProfileEditorCommand
|
#region Implementation of IProfileEditorCommand
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@ -43,7 +43,7 @@ public interface IProfileEditorService : IArtemisSharedUIService
|
|||||||
IObservable<int> PixelsPerSecond { get; }
|
IObservable<int> PixelsPerSecond { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Connect to the observable list of keyframes and observe any changes starting with the list's initial items.
|
/// Connect to the observable list of keyframes and observe any changes starting with the list's initial items.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>An observable which emits the change set.</returns>
|
/// <returns>An observable which emits the change set.</returns>
|
||||||
IObservable<IChangeSet<ILayerPropertyKeyframe>> ConnectToKeyframes();
|
IObservable<IChangeSet<ILayerPropertyKeyframe>> ConnectToKeyframes();
|
||||||
@ -73,18 +73,27 @@ public interface IProfileEditorService : IArtemisSharedUIService
|
|||||||
void ChangePixelsPerSecond(int pixelsPerSecond);
|
void ChangePixelsPerSecond(int pixelsPerSecond);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Selects the provided keyframe.
|
/// Selects the provided keyframe.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="keyframe">The keyframe to select.</param>
|
/// <param name="keyframe">The keyframe to select.</param>
|
||||||
/// <param name="expand">If <see langword="true"/> expands the current selection; otherwise replaces it with only the provided <paramref name="keyframe"/>.</param>
|
/// <param name="expand">
|
||||||
/// <param name="toggle">If <see langword="true"/> toggles the selection and only for the provided <paramref name="keyframe"/>.</param>
|
/// If <see langword="true" /> expands the current selection; otherwise replaces it with only the
|
||||||
|
/// provided <paramref name="keyframe" />.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="toggle">
|
||||||
|
/// If <see langword="true" /> toggles the selection and only for the provided
|
||||||
|
/// <paramref name="keyframe" />.
|
||||||
|
/// </param>
|
||||||
void SelectKeyframe(ILayerPropertyKeyframe? keyframe, bool expand, bool toggle);
|
void SelectKeyframe(ILayerPropertyKeyframe? keyframe, bool expand, bool toggle);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Selects the provided keyframes.
|
/// Selects the provided keyframes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="keyframes">The keyframes to select.</param>
|
/// <param name="keyframes">The keyframes to select.</param>
|
||||||
/// <param name="expand">If <see langword="true"/> expands the current selection; otherwise replaces it with only the provided <paramref name="keyframes"/>.</param>
|
/// <param name="expand">
|
||||||
|
/// If <see langword="true" /> expands the current selection; otherwise replaces it with only the
|
||||||
|
/// provided <paramref name="keyframes" />.
|
||||||
|
/// </param>
|
||||||
void SelectKeyframes(IEnumerable<ILayerPropertyKeyframe> keyframes, bool expand);
|
void SelectKeyframes(IEnumerable<ILayerPropertyKeyframe> keyframes, bool expand);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -99,12 +108,26 @@ public interface IProfileEditorService : IArtemisSharedUIService
|
|||||||
/// <returns>The snapped time.</returns>
|
/// <returns>The snapped time.</returns>
|
||||||
TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List<TimeSpan>? snapTimes = null);
|
TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List<TimeSpan>? snapTimes = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rounds the given time to something appropriate for the current zoom level.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="time">The time to round</param>
|
||||||
|
/// <returns>The rounded time.</returns>
|
||||||
|
TimeSpan RoundTime(TimeSpan time);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes the provided command and adds it to the history.
|
/// Executes the provided command and adds it to the history.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="command">The command to execute.</param>
|
/// <param name="command">The command to execute.</param>
|
||||||
void ExecuteCommand(IProfileEditorCommand command);
|
void ExecuteCommand(IProfileEditorCommand command);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new command scope which can be used to group undo/redo actions of multiple commands.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the command scope.</param>
|
||||||
|
/// <returns>The command scope that will group any commands until disposed.</returns>
|
||||||
|
ProfileEditorCommandScope CreateCommandScope(string name);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves the current profile.
|
/// Saves the current profile.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a scope in which editor commands are executed until disposed.
|
||||||
|
/// </summary>
|
||||||
|
public class ProfileEditorCommandScope : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<IProfileEditorCommand> _commands;
|
||||||
|
|
||||||
|
private readonly ProfileEditorService _profileEditorService;
|
||||||
|
|
||||||
|
internal ProfileEditorCommandScope(ProfileEditorService profileEditorService, string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
_profileEditorService = profileEditorService;
|
||||||
|
_commands = new List<IProfileEditorCommand>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the scope.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a read only collection of commands in the scope.
|
||||||
|
/// </summary>
|
||||||
|
public ReadOnlyCollection<IProfileEditorCommand> ProfileEditorCommands => new(_commands);
|
||||||
|
|
||||||
|
internal void AddCommand(IProfileEditorCommand command)
|
||||||
|
{
|
||||||
|
command.Execute();
|
||||||
|
_commands.Add(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_profileEditorService.StopCommandScope();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,94 +7,93 @@ using System.Reactive.Subjects;
|
|||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Services.ProfileEditor
|
namespace Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
|
|
||||||
|
public class ProfileEditorHistory
|
||||||
{
|
{
|
||||||
public class ProfileEditorHistory
|
private readonly Subject<bool> _canRedo = new();
|
||||||
|
private readonly Subject<bool> _canUndo = new();
|
||||||
|
private readonly Stack<IProfileEditorCommand> _redoCommands = new();
|
||||||
|
private readonly Stack<IProfileEditorCommand> _undoCommands = new();
|
||||||
|
|
||||||
|
public ProfileEditorHistory(ProfileConfiguration profileConfiguration)
|
||||||
{
|
{
|
||||||
private readonly Subject<bool> _canRedo = new();
|
ProfileConfiguration = profileConfiguration;
|
||||||
private readonly Subject<bool> _canUndo = new();
|
|
||||||
private readonly Stack<IProfileEditorCommand> _redoCommands = new();
|
|
||||||
private readonly Stack<IProfileEditorCommand> _undoCommands = new();
|
|
||||||
|
|
||||||
public ProfileEditorHistory(ProfileConfiguration profileConfiguration)
|
Execute = ReactiveCommand.Create<IProfileEditorCommand>(ExecuteEditorCommand);
|
||||||
{
|
Undo = ReactiveCommand.Create(ExecuteUndo, CanUndo);
|
||||||
ProfileConfiguration = profileConfiguration;
|
Redo = ReactiveCommand.Create(ExecuteRedo, CanRedo);
|
||||||
|
}
|
||||||
|
|
||||||
Execute = ReactiveCommand.Create<IProfileEditorCommand>(ExecuteEditorCommand);
|
public ProfileConfiguration ProfileConfiguration { get; }
|
||||||
Undo = ReactiveCommand.Create(ExecuteUndo, CanUndo);
|
public IObservable<bool> CanUndo => _canUndo.AsObservable().DistinctUntilChanged();
|
||||||
Redo = ReactiveCommand.Create(ExecuteRedo, CanRedo);
|
public IObservable<bool> CanRedo => _canRedo.AsObservable().DistinctUntilChanged();
|
||||||
}
|
|
||||||
|
|
||||||
public ProfileConfiguration ProfileConfiguration { get; }
|
public ReactiveCommand<IProfileEditorCommand, Unit> Execute { get; }
|
||||||
public IObservable<bool> CanUndo => _canUndo.AsObservable().DistinctUntilChanged();
|
public ReactiveCommand<Unit, IProfileEditorCommand?> Undo { get; }
|
||||||
public IObservable<bool> CanRedo => _canRedo.AsObservable().DistinctUntilChanged();
|
public ReactiveCommand<Unit, IProfileEditorCommand?> Redo { get; }
|
||||||
|
|
||||||
public ReactiveCommand<IProfileEditorCommand, Unit> Execute { get; }
|
public void Clear()
|
||||||
public ReactiveCommand<Unit, IProfileEditorCommand?> Undo { get; }
|
{
|
||||||
public ReactiveCommand<Unit, IProfileEditorCommand?> Redo { get; }
|
ClearRedo();
|
||||||
|
ClearUndo();
|
||||||
|
UpdateSubjects();
|
||||||
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void ExecuteEditorCommand(IProfileEditorCommand command)
|
||||||
{
|
{
|
||||||
ClearRedo();
|
command.Execute();
|
||||||
ClearUndo();
|
|
||||||
UpdateSubjects();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ExecuteEditorCommand(IProfileEditorCommand command)
|
_undoCommands.Push(command);
|
||||||
{
|
ClearRedo();
|
||||||
command.Execute();
|
UpdateSubjects();
|
||||||
|
}
|
||||||
|
|
||||||
_undoCommands.Push(command);
|
private void ClearRedo()
|
||||||
ClearRedo();
|
{
|
||||||
UpdateSubjects();
|
foreach (IProfileEditorCommand profileEditorCommand in _redoCommands)
|
||||||
}
|
if (profileEditorCommand is IDisposable disposable)
|
||||||
|
disposable.Dispose();
|
||||||
|
|
||||||
private void ClearRedo()
|
_redoCommands.Clear();
|
||||||
{
|
}
|
||||||
foreach (IProfileEditorCommand profileEditorCommand in _redoCommands)
|
|
||||||
if (profileEditorCommand is IDisposable disposable)
|
|
||||||
disposable.Dispose();
|
|
||||||
|
|
||||||
_redoCommands.Clear();
|
private void ClearUndo()
|
||||||
}
|
{
|
||||||
|
foreach (IProfileEditorCommand profileEditorCommand in _undoCommands)
|
||||||
|
if (profileEditorCommand is IDisposable disposable)
|
||||||
|
disposable.Dispose();
|
||||||
|
|
||||||
private void ClearUndo()
|
_undoCommands.Clear();
|
||||||
{
|
}
|
||||||
foreach (IProfileEditorCommand profileEditorCommand in _undoCommands)
|
|
||||||
if (profileEditorCommand is IDisposable disposable)
|
|
||||||
disposable.Dispose();
|
|
||||||
|
|
||||||
_undoCommands.Clear();
|
private IProfileEditorCommand? ExecuteUndo()
|
||||||
}
|
{
|
||||||
|
if (!_undoCommands.TryPop(out IProfileEditorCommand? command))
|
||||||
|
return null;
|
||||||
|
|
||||||
private IProfileEditorCommand? ExecuteUndo()
|
command.Undo();
|
||||||
{
|
_redoCommands.Push(command);
|
||||||
if (!_undoCommands.TryPop(out IProfileEditorCommand? command))
|
UpdateSubjects();
|
||||||
return null;
|
|
||||||
|
|
||||||
command.Undo();
|
return command;
|
||||||
_redoCommands.Push(command);
|
}
|
||||||
UpdateSubjects();
|
|
||||||
|
|
||||||
return command;
|
private IProfileEditorCommand? ExecuteRedo()
|
||||||
}
|
{
|
||||||
|
if (!_redoCommands.TryPop(out IProfileEditorCommand? command))
|
||||||
|
return null;
|
||||||
|
|
||||||
private IProfileEditorCommand? ExecuteRedo()
|
command.Execute();
|
||||||
{
|
_undoCommands.Push(command);
|
||||||
if (!_redoCommands.TryPop(out IProfileEditorCommand? command))
|
UpdateSubjects();
|
||||||
return null;
|
|
||||||
|
|
||||||
command.Execute();
|
return command;
|
||||||
_undoCommands.Push(command);
|
}
|
||||||
UpdateSubjects();
|
|
||||||
|
|
||||||
return command;
|
private void UpdateSubjects()
|
||||||
}
|
{
|
||||||
|
_canUndo.OnNext(_undoCommands.Any());
|
||||||
private void UpdateSubjects()
|
_canRedo.OnNext(_redoCommands.Any());
|
||||||
{
|
|
||||||
_canUndo.OnNext(_undoCommands.Any());
|
|
||||||
_canRedo.OnNext(_redoCommands.Any());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Shared.Services.Interfaces;
|
using Artemis.UI.Shared.Services.Interfaces;
|
||||||
|
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ internal class ProfileEditorService : IProfileEditorService
|
|||||||
private readonly IProfileService _profileService;
|
private readonly IProfileService _profileService;
|
||||||
private readonly IModuleService _moduleService;
|
private readonly IModuleService _moduleService;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
|
private ProfileEditorCommandScope? _profileEditorHistoryScope;
|
||||||
|
|
||||||
public ProfileEditorService(ILogger logger, IProfileService profileService, IModuleService moduleService, IWindowService windowService)
|
public ProfileEditorService(ILogger logger, IProfileService profileService, IModuleService moduleService, IWindowService windowService)
|
||||||
{
|
{
|
||||||
@ -86,6 +88,9 @@ internal class ProfileEditorService : IProfileEditorService
|
|||||||
// Deselect whatever profile element was active
|
// Deselect whatever profile element was active
|
||||||
ChangeCurrentProfileElement(null);
|
ChangeCurrentProfileElement(null);
|
||||||
|
|
||||||
|
// Close the command scope if one was open
|
||||||
|
_profileEditorHistoryScope?.Dispose();
|
||||||
|
|
||||||
// The new profile may need activation
|
// The new profile may need activation
|
||||||
if (profileConfiguration != null)
|
if (profileConfiguration != null)
|
||||||
{
|
{
|
||||||
@ -205,11 +210,27 @@ internal class ProfileEditorService : IProfileEditorService
|
|||||||
return time;
|
return time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TimeSpan RoundTime(TimeSpan time)
|
||||||
|
{
|
||||||
|
// Round the time to something that fits the current zoom level
|
||||||
|
if (_pixelsPerSecondSubject.Value < 50)
|
||||||
|
return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 200.0) * 200.0);
|
||||||
|
if (_pixelsPerSecondSubject.Value < 100)
|
||||||
|
return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 100.0) * 100.0);
|
||||||
|
if (_pixelsPerSecondSubject.Value < 200)
|
||||||
|
return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 50.0) * 50.0);
|
||||||
|
if (_pixelsPerSecondSubject.Value < 500)
|
||||||
|
return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 20.0) * 20.0);
|
||||||
|
return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds));
|
||||||
|
}
|
||||||
|
|
||||||
public void ChangePixelsPerSecond(int pixelsPerSecond)
|
public void ChangePixelsPerSecond(int pixelsPerSecond)
|
||||||
{
|
{
|
||||||
_pixelsPerSecondSubject.OnNext(pixelsPerSecond);
|
_pixelsPerSecondSubject.OnNext(pixelsPerSecond);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Commands
|
||||||
|
|
||||||
public void ExecuteCommand(IProfileEditorCommand command)
|
public void ExecuteCommand(IProfileEditorCommand command)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -218,6 +239,13 @@ internal class ProfileEditorService : IProfileEditorService
|
|||||||
if (history == null)
|
if (history == null)
|
||||||
throw new ArtemisSharedUIException("Can't execute a command when there's no active profile configuration");
|
throw new ArtemisSharedUIException("Can't execute a command when there's no active profile configuration");
|
||||||
|
|
||||||
|
// If a scope is active add the command to it, the scope will execute it immediately
|
||||||
|
if (_profileEditorHistoryScope != null)
|
||||||
|
{
|
||||||
|
_profileEditorHistoryScope.AddCommand(command);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
history.Execute.Execute(command).Subscribe();
|
history.Execute.Execute(command).Subscribe();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -227,6 +255,36 @@ internal class ProfileEditorService : IProfileEditorService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ProfileEditorCommandScope CreateCommandScope(string name)
|
||||||
|
{
|
||||||
|
if (_profileEditorHistoryScope != null)
|
||||||
|
throw new ArtemisSharedUIException($"A command scope is already active, name: {_profileEditorHistoryScope.Name}.");
|
||||||
|
|
||||||
|
if (name == null)
|
||||||
|
throw new ArgumentNullException(nameof(name));
|
||||||
|
|
||||||
|
_profileEditorHistoryScope = new ProfileEditorCommandScope(this, name);
|
||||||
|
return _profileEditorHistoryScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void StopCommandScope()
|
||||||
|
{
|
||||||
|
// This might happen if the scope is disposed twice, it's no biggie
|
||||||
|
if (_profileEditorHistoryScope == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ProfileEditorCommandScope scope = _profileEditorHistoryScope;
|
||||||
|
_profileEditorHistoryScope = null;
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SaveProfile()
|
public void SaveProfile()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -72,7 +72,7 @@ public class PropertiesView : ReactiveUserControl<PropertiesViewModel>
|
|||||||
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
||||||
{
|
{
|
||||||
List<TimeSpan> snapTimes = ViewModel.PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Select(k => k.Position).ToList();
|
List<TimeSpan> snapTimes = ViewModel.PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Select(k => k.Position).ToList();
|
||||||
newTime = ViewModel.TimelineViewModel.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ViewModel.PixelsPerSecond * 5), true, false, snapTimes);
|
newTime = ViewModel.TimelineViewModel.SnapToTimeline(newTime, true, false, snapTimes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If holding down control, round to the closest 50ms
|
// If holding down control, round to the closest 50ms
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
|
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||||
|
|
||||||
@ -12,20 +13,20 @@ public interface ITimelineKeyframeViewModel
|
|||||||
#region Movement
|
#region Movement
|
||||||
|
|
||||||
void Select(bool expand, bool toggle);
|
void Select(bool expand, bool toggle);
|
||||||
// void StartMovement();
|
void StartMovement(ITimelineKeyframeViewModel source);
|
||||||
// void FinishMovement();
|
void UpdateMovement(TimeSpan position);
|
||||||
|
void FinishMovement();
|
||||||
void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source);
|
TimeSpan GetTimeSpanAtPosition(double x);
|
||||||
void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source);
|
|
||||||
void UpdatePosition(TimeSpan position);
|
|
||||||
void ReleaseMovement();
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Context menu actions
|
#region Context menu actions
|
||||||
|
|
||||||
void PopulateEasingViewModels();
|
void PopulateEasingViewModels();
|
||||||
void Delete(bool save = true);
|
void Duplicate();
|
||||||
|
void Copy();
|
||||||
|
void Paste();
|
||||||
|
void Delete();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
@ -17,7 +17,8 @@
|
|||||||
Classes.selected="{Binding IsSelected}"
|
Classes.selected="{Binding IsSelected}"
|
||||||
PointerPressed="InputElement_OnPointerPressed"
|
PointerPressed="InputElement_OnPointerPressed"
|
||||||
PointerReleased="InputElement_OnPointerReleased"
|
PointerReleased="InputElement_OnPointerReleased"
|
||||||
PointerMoved="InputElement_OnPointerMoved">
|
PointerMoved="InputElement_OnPointerMoved"
|
||||||
|
Cursor="Hand">
|
||||||
<Ellipse.Styles>
|
<Ellipse.Styles>
|
||||||
<Style Selector="Ellipse">
|
<Style Selector="Ellipse">
|
||||||
<Setter Property="StrokeThickness" Value="0" />
|
<Setter Property="StrokeThickness" Value="0" />
|
||||||
@ -52,23 +53,23 @@
|
|||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="Duplicate" Command="{Binding DuplicateKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+D">
|
<MenuItem Header="Duplicate" Command="{Binding $parent[timeline:TimelineView].DataContext.DuplicateKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+D">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<avalonia:MaterialIcon Kind="ContentDuplicate" />
|
<avalonia:MaterialIcon Kind="ContentDuplicate" />
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem Header="Copy" Command="{Binding CopyKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+C">
|
<MenuItem Header="Copy" Command="{Binding $parent[timeline:TimelineView].DataContext.CopyKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+C">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<avalonia:MaterialIcon Kind="ContentCopy" />
|
<avalonia:MaterialIcon Kind="ContentCopy" />
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem Header="Paste" Command="{Binding PasteKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+V">
|
<MenuItem Header="Paste" Command="{Binding $parent[timeline:TimelineView].DataContext.PasteKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+V">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<avalonia:MaterialIcon Kind="ContentPaste" />
|
<avalonia:MaterialIcon Kind="ContentPaste" />
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="Delete" Command="{Binding DeleteKeyframes}" InputGesture="Delete">
|
<MenuItem Header="Delete" Command="{Binding $parent[timeline:TimelineView].DataContext.DeleteKeyframes}" CommandParameter="{Binding}" InputGesture="Delete">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<avalonia:MaterialIcon Kind="Delete" />
|
<avalonia:MaterialIcon Kind="Delete" />
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
|
using Avalonia.LogicalTree;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
@ -7,6 +8,8 @@ namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
|||||||
|
|
||||||
public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewModel>
|
public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewModel>
|
||||||
{
|
{
|
||||||
|
private TimelinePropertyView? _timelinePropertyView;
|
||||||
|
private TimelineView? _timelineView;
|
||||||
private bool _moved;
|
private bool _moved;
|
||||||
|
|
||||||
public TimelineKeyframeView()
|
public TimelineKeyframeView()
|
||||||
@ -14,6 +17,19 @@ public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewMod
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Overrides of TemplatedControl
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
_timelineView = this.FindLogicalAncestorOfType<TimelineView>();
|
||||||
|
_timelinePropertyView = this.FindLogicalAncestorOfType<TimelinePropertyView>();
|
||||||
|
|
||||||
|
base.OnAttachedToLogicalTree(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
@ -21,6 +37,9 @@ public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewMod
|
|||||||
|
|
||||||
private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||||
|
return;
|
||||||
|
|
||||||
e.Pointer.Capture((IInputElement?) sender);
|
e.Pointer.Capture((IInputElement?) sender);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
|
|
||||||
@ -29,20 +48,34 @@ public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewMod
|
|||||||
|
|
||||||
private void InputElement_OnPointerMoved(object? sender, PointerEventArgs e)
|
private void InputElement_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Pointer.Captured != sender)
|
if (ViewModel == null || _timelineView?.ViewModel == null || e.Pointer.Captured != sender || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (!_moved)
|
||||||
|
_timelineView.ViewModel.StartKeyframeMovement(ViewModel);
|
||||||
_moved = true;
|
_moved = true;
|
||||||
|
|
||||||
|
TimeSpan time = ViewModel.GetTimeSpanAtPosition(e.GetPosition(_timelinePropertyView).X);
|
||||||
|
_timelineView.ViewModel.UpdateKeyframeMovement(ViewModel, time, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
|
||||||
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (ViewModel == null || _timelineView?.ViewModel == null || e.Pointer.Captured != sender || e.InitialPressMouseButton != MouseButton.Left)
|
||||||
|
return;
|
||||||
|
|
||||||
e.Pointer.Capture(null);
|
e.Pointer.Capture(null);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
|
|
||||||
// Select the keyframe if the user didn't move
|
// Select the keyframe if the user didn't move
|
||||||
if (!_moved)
|
if (!_moved)
|
||||||
ViewModel?.Select(e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
|
ViewModel.Select(e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TimeSpan time = ViewModel.GetTimeSpanAtPosition(e.GetPosition(_timelinePropertyView).X);
|
||||||
|
_timelineView.ViewModel.FinishKeyframeMovement(ViewModel, time, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FlyoutBase_OnOpening(object? sender, EventArgs e)
|
private void FlyoutBase_OnOpening(object? sender, EventArgs e)
|
||||||
|
|||||||
@ -5,10 +5,10 @@ using System.Reactive.Linq;
|
|||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
|
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
using Avalonia.Controls.Mixins;
|
using Avalonia.Controls.Mixins;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using Humanizer;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
|
||||||
@ -30,14 +30,15 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
|
|||||||
|
|
||||||
this.WhenActivated(d =>
|
this.WhenActivated(d =>
|
||||||
{
|
{
|
||||||
|
_isSelected = profileEditorService.ConnectToKeyframes().ToCollection().Select(keyframes => keyframes.Contains(LayerPropertyKeyframe)).ToProperty(this, vm => vm.IsSelected).DisposeWith(d);
|
||||||
|
profileEditorService.ConnectToKeyframes();
|
||||||
profileEditorService.PixelsPerSecond.Subscribe(p =>
|
profileEditorService.PixelsPerSecond.Subscribe(p =>
|
||||||
{
|
{
|
||||||
_pixelsPerSecond = p;
|
_pixelsPerSecond = p;
|
||||||
profileEditorService.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d);
|
profileEditorService.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d);
|
||||||
}).DisposeWith(d);
|
}).DisposeWith(d);
|
||||||
|
|
||||||
_isSelected = profileEditorService.ConnectToKeyframes().ToCollection().Select(keyframes => keyframes.Contains(LayerPropertyKeyframe)).ToProperty(this, vm => vm.IsSelected).DisposeWith(d);
|
this.WhenAnyValue(vm => vm.LayerPropertyKeyframe.Position).Subscribe(_ => Update()).DisposeWith(d);
|
||||||
profileEditorService.ConnectToKeyframes();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,60 +73,76 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
|
|||||||
_profileEditorService.SelectKeyframe(Keyframe, expand, toggle);
|
_profileEditorService.SelectKeyframe(Keyframe, expand, toggle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TimeSpan GetTimeSpanAtPosition(double x)
|
||||||
|
{
|
||||||
|
return TimeSpan.FromSeconds(x / _pixelsPerSecond);
|
||||||
|
}
|
||||||
|
|
||||||
#region Context menu actions
|
#region Context menu actions
|
||||||
|
|
||||||
public void Delete(bool save = true)
|
public void Duplicate()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Copy()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Paste()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Delete()
|
||||||
|
{
|
||||||
|
_profileEditorService.ExecuteCommand(new DeleteKeyframe<T>(LayerPropertyKeyframe));
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
#region Movement
|
#region Movement
|
||||||
|
|
||||||
private TimeSpan? _offset;
|
private TimeSpan? _offset;
|
||||||
|
private TimeSpan? _startPosition;
|
||||||
private double _pixelsPerSecond;
|
private double _pixelsPerSecond;
|
||||||
|
|
||||||
public void ReleaseMovement()
|
public void StartMovement(ITimelineKeyframeViewModel source)
|
||||||
{
|
{
|
||||||
|
_startPosition = LayerPropertyKeyframe.Position;
|
||||||
|
if (source != this)
|
||||||
|
_offset = LayerPropertyKeyframe.Position - source.Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateMovement(TimeSpan position)
|
||||||
|
{
|
||||||
|
if (_offset == null)
|
||||||
|
UpdatePosition(position);
|
||||||
|
else
|
||||||
|
UpdatePosition(position + _offset.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FinishMovement()
|
||||||
|
{
|
||||||
|
MoveKeyframe command = _startPosition != null
|
||||||
|
? new MoveKeyframe(Keyframe, Keyframe.Position, _startPosition.Value)
|
||||||
|
: new MoveKeyframe(Keyframe, Keyframe.Position);
|
||||||
|
|
||||||
|
_startPosition = null;
|
||||||
_offset = null;
|
_offset = null;
|
||||||
|
|
||||||
|
_profileEditorService.ExecuteCommand(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source)
|
private void UpdatePosition(TimeSpan position)
|
||||||
{
|
{
|
||||||
if (source == this)
|
if (position < TimeSpan.Zero)
|
||||||
{
|
LayerPropertyKeyframe.Position = TimeSpan.Zero;
|
||||||
_offset = null;
|
else if (position > LayerPropertyKeyframe.LayerProperty.ProfileElement.Timeline.Length)
|
||||||
return;
|
LayerPropertyKeyframe.Position = LayerPropertyKeyframe.LayerProperty.ProfileElement.Timeline.Length;
|
||||||
}
|
else
|
||||||
|
LayerPropertyKeyframe.Position = position;
|
||||||
if (_offset != null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_offset = LayerPropertyKeyframe.Position - source.Position;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source)
|
|
||||||
{
|
|
||||||
if (source == this || _offset == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
UpdatePosition(source.Position + _offset.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdatePosition(TimeSpan position)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
|
|
||||||
// if (position < TimeSpan.Zero)
|
|
||||||
// LayerPropertyKeyframe.Position = TimeSpan.Zero;
|
|
||||||
// else if (position > _profileEditorService.SelectedProfileElement.Timeline.Length)
|
|
||||||
// LayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.Timeline.Length;
|
|
||||||
// else
|
|
||||||
// LayerPropertyKeyframe.Position = position;
|
|
||||||
|
|
||||||
Update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -150,35 +167,3 @@ public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineK
|
|||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ChangeKeyframeEasing : IProfileEditorCommand
|
|
||||||
{
|
|
||||||
private readonly ILayerPropertyKeyframe _keyframe;
|
|
||||||
private readonly Easings.Functions _easingFunction;
|
|
||||||
private readonly Easings.Functions _originalEasingFunction;
|
|
||||||
|
|
||||||
public ChangeKeyframeEasing(ILayerPropertyKeyframe keyframe, Easings.Functions easingFunction)
|
|
||||||
{
|
|
||||||
_keyframe = keyframe;
|
|
||||||
_easingFunction = easingFunction;
|
|
||||||
_originalEasingFunction = keyframe.EasingFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Implementation of IProfileEditorCommand
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string DisplayName => "Change easing to " + _easingFunction.Humanize(LetterCasing.LowerCase);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Execute()
|
|
||||||
{
|
|
||||||
_keyframe.EasingFunction = _easingFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Undo()
|
|
||||||
{
|
|
||||||
_keyframe.EasingFunction = _originalEasingFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
|
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
using Avalonia.Controls.Mixins;
|
using Avalonia.Controls.Mixins;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
@ -14,6 +15,8 @@ public class TimelineViewModel : ActivatableViewModelBase
|
|||||||
{
|
{
|
||||||
private readonly IProfileEditorService _profileEditorService;
|
private readonly IProfileEditorService _profileEditorService;
|
||||||
private ObservableAsPropertyHelper<double>? _caretPosition;
|
private ObservableAsPropertyHelper<double>? _caretPosition;
|
||||||
|
private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
|
||||||
|
private List<ITimelineKeyframeViewModel>? _moveKeyframes;
|
||||||
|
|
||||||
public TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels, IProfileEditorService profileEditorService)
|
public TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels, IProfileEditorService profileEditorService)
|
||||||
{
|
{
|
||||||
@ -26,20 +29,23 @@ public class TimelineViewModel : ActivatableViewModelBase
|
|||||||
.CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
.CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
|
||||||
.ToProperty(this, vm => vm.CaretPosition)
|
.ToProperty(this, vm => vm.CaretPosition)
|
||||||
.DisposeWith(d);
|
.DisposeWith(d);
|
||||||
|
|
||||||
|
_pixelsPerSecond = _profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
|
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
|
||||||
|
|
||||||
public double CaretPosition => _caretPosition?.Value ?? 0.0;
|
public double CaretPosition => _caretPosition?.Value ?? 0.0;
|
||||||
|
public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
|
||||||
|
|
||||||
public void ChangeTime(TimeSpan newTime)
|
public void ChangeTime(TimeSpan newTime)
|
||||||
{
|
{
|
||||||
_profileEditorService.ChangeTime(newTime);
|
_profileEditorService.ChangeTime(newTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List<TimeSpan>? snapTimes = null)
|
public TimeSpan SnapToTimeline(TimeSpan time, bool snapToSegments, bool snapToCurrentTime, List<TimeSpan>? snapTimes = null)
|
||||||
{
|
{
|
||||||
|
TimeSpan tolerance = TimeSpan.FromMilliseconds(1000f / PixelsPerSecond * 5);
|
||||||
return _profileEditorService.SnapToTimeline(time, tolerance, snapToSegments, snapToCurrentTime, snapTimes);
|
return _profileEditorService.SnapToTimeline(time, tolerance, snapToSegments, snapToCurrentTime, snapTimes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,4 +53,122 @@ public class TimelineViewModel : ActivatableViewModelBase
|
|||||||
{
|
{
|
||||||
_profileEditorService.SelectKeyframes(keyframes.Select(k => k.Keyframe), expand);
|
_profileEditorService.SelectKeyframes(keyframes.Select(k => k.Keyframe), expand);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Keyframe movement
|
||||||
|
|
||||||
|
public void StartKeyframeMovement(ITimelineKeyframeViewModel source)
|
||||||
|
{
|
||||||
|
if (!source.IsSelected)
|
||||||
|
SelectKeyframes(new List<ITimelineKeyframeViewModel> {source}, false);
|
||||||
|
|
||||||
|
_moveKeyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
|
||||||
|
|
||||||
|
source.StartMovement(source);
|
||||||
|
foreach (ITimelineKeyframeViewModel keyframeViewModel in _moveKeyframes)
|
||||||
|
keyframeViewModel.StartMovement(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateKeyframeMovement(ITimelineKeyframeViewModel source, TimeSpan time, bool snap, bool round)
|
||||||
|
{
|
||||||
|
if (_moveKeyframes == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (round)
|
||||||
|
time = _profileEditorService.RoundTime(time);
|
||||||
|
if (snap)
|
||||||
|
time = SnapToTimeline(time, true, true);
|
||||||
|
|
||||||
|
// Always update source first
|
||||||
|
source.UpdateMovement(time);
|
||||||
|
foreach (ITimelineKeyframeViewModel keyframeViewModel in _moveKeyframes)
|
||||||
|
keyframeViewModel.UpdateMovement(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void FinishKeyframeMovement(ITimelineKeyframeViewModel source, TimeSpan time, bool snap, bool round)
|
||||||
|
{
|
||||||
|
if (_moveKeyframes == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (round)
|
||||||
|
time = _profileEditorService.RoundTime(time);
|
||||||
|
if (snap)
|
||||||
|
time = SnapToTimeline(time, true, true);
|
||||||
|
|
||||||
|
// If only one selected it's always the source, update it as a single command
|
||||||
|
if (_moveKeyframes.Count == 1)
|
||||||
|
{
|
||||||
|
source.UpdateMovement(time);
|
||||||
|
source.FinishMovement();
|
||||||
|
}
|
||||||
|
// Otherwise update in a scope
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Move {_moveKeyframes.Count} keyframes.");
|
||||||
|
// Always update source first
|
||||||
|
source.UpdateMovement(time);
|
||||||
|
foreach (ITimelineKeyframeViewModel keyframeViewModel in _moveKeyframes)
|
||||||
|
{
|
||||||
|
keyframeViewModel.UpdateMovement(time);
|
||||||
|
keyframeViewModel.FinishMovement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Keyframe actions
|
||||||
|
|
||||||
|
public void DuplicateKeyframes(ITimelineKeyframeViewModel source)
|
||||||
|
{
|
||||||
|
if (!source.IsSelected)
|
||||||
|
source.Duplicate();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
|
||||||
|
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Duplicate {keyframes.Count} keyframes.");
|
||||||
|
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
|
||||||
|
timelineKeyframeViewModel.Duplicate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyKeyframes(ITimelineKeyframeViewModel source)
|
||||||
|
{
|
||||||
|
if (!source.IsSelected)
|
||||||
|
source.Copy();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
|
||||||
|
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Copy {keyframes.Count} keyframes.");
|
||||||
|
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
|
||||||
|
timelineKeyframeViewModel.Copy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PasteKeyframes(ITimelineKeyframeViewModel source)
|
||||||
|
{
|
||||||
|
if (!source.IsSelected)
|
||||||
|
source.Paste();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
|
||||||
|
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Paste {keyframes.Count} keyframes.");
|
||||||
|
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
|
||||||
|
timelineKeyframeViewModel.Paste();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteKeyframes(ITimelineKeyframeViewModel source)
|
||||||
|
{
|
||||||
|
if (!source.IsSelected)
|
||||||
|
source.Delete();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
|
||||||
|
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Delete {keyframes.Count} keyframes.");
|
||||||
|
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
|
||||||
|
timelineKeyframeViewModel.Delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user