mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-12 03:48:56 +00:00
Profiles - Added module activation requirements
Editor - Refactored tools and selected keyframes management Timeline - Added keyframe duplicating, copying and pasting Windows UI - Added logging of fatal exceptions
This commit is contained in:
1 parent
d563d17270
commit
dd40bdd544
31 files changed
+719
-178
No files matched your search
@@ -0,0 +1,29 @@
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Avalonia.Input.Platform;
|
||||
|
||||
namespace Artemis.UI.Shared.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for Avalonia's <see cref="IClipboard" /> type.
|
||||
/// </summary>
|
||||
public static class ClipboardExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves clipboard JSON data representing <typeparamref name="T" /> and deserializes it into an instance of
|
||||
/// <typeparamref name="T" />.
|
||||
/// </summary>
|
||||
/// <param name="clipboard">The clipboard to retrieve the data off.</param>
|
||||
/// <param name="format">The data format to retrieve data for.</param>
|
||||
/// <typeparam name="T">The type of data to retrieve</typeparam>
|
||||
/// <returns>
|
||||
/// The resulting value or if the clipboard did not contain data for the provided <paramref name="format" />;
|
||||
/// <see langword="null" />.
|
||||
/// </returns>
|
||||
public static async Task<T?> GetJsonAsync<T>(this IClipboard clipboard, string format)
|
||||
{
|
||||
byte[]? bytes = (byte[]?) await clipboard.GetDataAsync(format);
|
||||
return bytes == null ? default : CoreJson.DeserializeObject<T>(Encoding.Unicode.GetString(bytes), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using Artemis.Core;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a profile editor command that can be used to duplicate a keyframe at a new position.
|
||||
/// </summary>
|
||||
public class DuplicateKeyframe : IProfileEditorCommand
|
||||
{
|
||||
private readonly ILayerPropertyKeyframe _keyframe;
|
||||
private readonly TimeSpan _position;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the duplicated keyframe, only available after the command has been executed.
|
||||
/// </summary>
|
||||
public ILayerPropertyKeyframe? Duplication { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="DeleteKeyframe" /> class.
|
||||
/// </summary>
|
||||
/// <param name="keyframe">The keyframe to duplicate.</param>
|
||||
/// <param name="position">The position of the duplicated keyframe.</param>
|
||||
public DuplicateKeyframe(ILayerPropertyKeyframe keyframe, TimeSpan position)
|
||||
{
|
||||
_keyframe = keyframe;
|
||||
_position = position;
|
||||
}
|
||||
|
||||
#region Implementation of IProfileEditorCommand
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName => "Duplicate keyframe";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute()
|
||||
{
|
||||
if (Duplication == null)
|
||||
{
|
||||
Duplication = _keyframe.CreateCopy();
|
||||
Duplication.Position = _position;
|
||||
}
|
||||
|
||||
_keyframe.UntypedLayerProperty.AddUntypedKeyframe(Duplication);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Undo()
|
||||
{
|
||||
if (Duplication != null)
|
||||
_keyframe.UntypedLayerProperty.RemoveUntypedKeyframe(Duplication);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using DynamicData;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.ProfileEditor;
|
||||
|
||||
@@ -52,15 +52,14 @@ public interface IProfileEditorService : IArtemisSharedUIService
|
||||
IObservable<bool> SuspendedEditing { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a source list of all available editor tools.
|
||||
/// Gets an observable read only collection of all available editor tools.
|
||||
/// </summary>
|
||||
SourceList<IToolViewModel> Tools { get; }
|
||||
ReadOnlyObservableCollection<IToolViewModel> Tools { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Connect to the observable list of keyframes and observe any changes starting with the list's initial items.
|
||||
/// Gets an observable read only collection of selected keyframes.
|
||||
/// </summary>
|
||||
/// <returns>An observable which emits the change set.</returns>
|
||||
IObservable<IChangeSet<ILayerPropertyKeyframe>> ConnectToKeyframes();
|
||||
ReadOnlyObservableCollection<ILayerPropertyKeyframe> SelectedKeyframes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Changes the selected profile by its <see cref="Core.ProfileConfiguration" />.
|
||||
@@ -188,4 +187,16 @@ public interface IProfileEditorService : IArtemisSharedUIService
|
||||
/// Pauses profile preview playback.
|
||||
/// </summary>
|
||||
void Pause();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a profile editor tool by it's view model.
|
||||
/// </summary>
|
||||
/// <param name="toolViewModel">The view model of the tool to add.</param>
|
||||
void AddTool(IToolViewModel toolViewModel);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a profile editor tool by it's view model.
|
||||
/// </summary>
|
||||
/// <param name="toolViewModel">The view model of the tool to remove.</param>
|
||||
void RemoveTool(IToolViewModel toolViewModel);
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
using Material.Icons;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.ProfileEditor;
|
||||
|
||||
@@ -47,6 +45,7 @@ public interface IToolViewModel : IDisposable
|
||||
public string ToolTip { get; }
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IToolViewModel" />
|
||||
public abstract class ToolViewModel : ActivatableViewModelBase, IToolViewModel
|
||||
{
|
||||
private bool _isSelected;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
@@ -26,9 +27,10 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
private readonly Dictionary<ProfileConfiguration, ProfileEditorHistory> _profileEditorHistories = new();
|
||||
private readonly BehaviorSubject<RenderProfileElement?> _profileElementSubject = new(null);
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly SourceList<ILayerPropertyKeyframe> _selectedKeyframes = new();
|
||||
private readonly BehaviorSubject<bool> _suspendedEditingSubject = new(false);
|
||||
private readonly BehaviorSubject<TimeSpan> _timeSubject = new(TimeSpan.Zero);
|
||||
private readonly SourceList<IToolViewModel> _tools;
|
||||
private readonly SourceList<ILayerPropertyKeyframe> _selectedKeyframes;
|
||||
private readonly IWindowService _windowService;
|
||||
private ProfileEditorCommandScope? _profileEditorHistoryScope;
|
||||
|
||||
@@ -46,6 +48,12 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
_layerBrushService = layerBrushService;
|
||||
_windowService = windowService;
|
||||
|
||||
_tools = new SourceList<IToolViewModel>();
|
||||
_selectedKeyframes = new SourceList<ILayerPropertyKeyframe>();
|
||||
_tools.Connect().AutoRefreshOnObservable(t => t.WhenAnyValue(vm => vm.IsSelected)).Subscribe(OnToolSelected);
|
||||
_tools.Connect().Bind(out ReadOnlyObservableCollection<IToolViewModel> tools).Subscribe();
|
||||
_selectedKeyframes.Connect().Bind(out ReadOnlyObservableCollection<ILayerPropertyKeyframe> selectedKeyframes).Subscribe();
|
||||
|
||||
ProfileConfiguration = _profileConfigurationSubject.AsObservable();
|
||||
ProfileElement = _profileElementSubject.AsObservable();
|
||||
LayerProperty = _layerPropertySubject.AsObservable();
|
||||
@@ -54,60 +62,8 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
Playing = _playingSubject.AsObservable();
|
||||
SuspendedEditing = _suspendedEditingSubject.AsObservable();
|
||||
PixelsPerSecond = _pixelsPerSecondSubject.AsObservable();
|
||||
Tools = new SourceList<IToolViewModel>();
|
||||
Tools.Connect().AutoRefreshOnObservable(t => t.WhenAnyValue(vm => vm.IsSelected)).Subscribe(set =>
|
||||
{
|
||||
IToolViewModel? changed = set.FirstOrDefault()?.Item.Current;
|
||||
if (changed == null)
|
||||
return;
|
||||
|
||||
// Disable all others if the changed one is selected and exclusive
|
||||
if (changed.IsSelected && changed.IsExclusive)
|
||||
Tools.Edit(list =>
|
||||
{
|
||||
foreach (IToolViewModel toolViewModel in list.Where(t => t.IsExclusive && t != changed))
|
||||
toolViewModel.IsSelected = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration)
|
||||
{
|
||||
if (profileConfiguration == null)
|
||||
return null;
|
||||
if (_profileEditorHistories.TryGetValue(profileConfiguration, out ProfileEditorHistory? history))
|
||||
return history;
|
||||
|
||||
ProfileEditorHistory newHistory = new(profileConfiguration);
|
||||
_profileEditorHistories.Add(profileConfiguration, newHistory);
|
||||
return newHistory;
|
||||
}
|
||||
|
||||
private void Tick(TimeSpan time)
|
||||
{
|
||||
if (_profileConfigurationSubject.Value?.Profile == null || _suspendedEditingSubject.Value)
|
||||
return;
|
||||
|
||||
TickProfileElement(_profileConfigurationSubject.Value.Profile.GetRootFolder(), time);
|
||||
}
|
||||
|
||||
private void TickProfileElement(ProfileElement profileElement, TimeSpan time)
|
||||
{
|
||||
if (profileElement is not RenderProfileElement renderElement)
|
||||
return;
|
||||
|
||||
if (renderElement.Suspended)
|
||||
{
|
||||
renderElement.Disable();
|
||||
}
|
||||
else
|
||||
{
|
||||
renderElement.Enable();
|
||||
renderElement.OverrideTimelineAndApply(time);
|
||||
|
||||
foreach (ProfileElement child in renderElement.Children)
|
||||
TickProfileElement(child, time);
|
||||
}
|
||||
Tools = tools;
|
||||
SelectedKeyframes = selectedKeyframes;
|
||||
}
|
||||
|
||||
public IObservable<ProfileConfiguration?> ProfileConfiguration { get; }
|
||||
@@ -118,12 +74,8 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
public IObservable<TimeSpan> Time { get; }
|
||||
public IObservable<bool> Playing { get; }
|
||||
public IObservable<int> PixelsPerSecond { get; }
|
||||
public SourceList<IToolViewModel> Tools { get; }
|
||||
|
||||
public IObservable<IChangeSet<ILayerPropertyKeyframe>> ConnectToKeyframes()
|
||||
{
|
||||
return _selectedKeyframes.Connect();
|
||||
}
|
||||
public ReadOnlyObservableCollection<IToolViewModel> Tools { get; }
|
||||
public ReadOnlyObservableCollection<ILayerPropertyKeyframe> SelectedKeyframes { get; }
|
||||
|
||||
public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
|
||||
{
|
||||
@@ -318,7 +270,7 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public Layer CreateAndAddLayer(ProfileElement target)
|
||||
{
|
||||
@@ -380,6 +332,18 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
_playingSubject.OnNext(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddTool(IToolViewModel toolViewModel)
|
||||
{
|
||||
_tools.Add(toolViewModel);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RemoveTool(IToolViewModel toolViewModel)
|
||||
{
|
||||
_tools.Remove(toolViewModel);
|
||||
}
|
||||
|
||||
#region Commands
|
||||
|
||||
public void ExecuteCommand(IProfileEditorCommand command)
|
||||
@@ -433,4 +397,58 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void OnToolSelected(IChangeSet<IToolViewModel> changeSet)
|
||||
{
|
||||
IToolViewModel? changed = changeSet.FirstOrDefault()?.Item.Current;
|
||||
if (changed == null)
|
||||
return;
|
||||
|
||||
// Disable all others if the changed one is selected and exclusive
|
||||
if (changed.IsSelected && changed.IsExclusive)
|
||||
_tools.Edit(list =>
|
||||
{
|
||||
foreach (IToolViewModel toolViewModel in list.Where(t => t.IsExclusive && t != changed))
|
||||
toolViewModel.IsSelected = false;
|
||||
});
|
||||
}
|
||||
|
||||
private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration)
|
||||
{
|
||||
if (profileConfiguration == null)
|
||||
return null;
|
||||
if (_profileEditorHistories.TryGetValue(profileConfiguration, out ProfileEditorHistory? history))
|
||||
return history;
|
||||
|
||||
ProfileEditorHistory newHistory = new(profileConfiguration);
|
||||
_profileEditorHistories.Add(profileConfiguration, newHistory);
|
||||
return newHistory;
|
||||
}
|
||||
|
||||
private void Tick(TimeSpan time)
|
||||
{
|
||||
if (_profileConfigurationSubject.Value?.Profile == null || _suspendedEditingSubject.Value)
|
||||
return;
|
||||
|
||||
TickProfileElement(_profileConfigurationSubject.Value.Profile.GetRootFolder(), time);
|
||||
}
|
||||
|
||||
private void TickProfileElement(ProfileElement profileElement, TimeSpan time)
|
||||
{
|
||||
if (profileElement is not RenderProfileElement renderElement)
|
||||
return;
|
||||
|
||||
if (renderElement.Suspended)
|
||||
{
|
||||
renderElement.Disable();
|
||||
}
|
||||
else
|
||||
{
|
||||
renderElement.Enable();
|
||||
renderElement.OverrideTimelineAndApply(time);
|
||||
|
||||
foreach (ProfileElement child in renderElement.Children)
|
||||
TickProfileElement(child, time);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user